OSDN Git Service

Initial commit
[rebornos/cnchi-gnome-mac-osdn.git] / Cnchi / metalink.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 #  metalink.py
5 #
6 #  Parts of code from pm2ml Copyright (C) 2012-2013 Xyne
7 #  Copyright © 2013-2018 Antergos
8 #
9 #  This file is part of Cnchi.
10 #
11 #  Cnchi is free software; you can redistribute it and/or modify
12 #  it under the terms of the GNU General Public License as published by
13 #  the Free Software Foundation; either version 3 of the License, or
14 #  (at your option) any later version.
15 #
16 #  Cnchi is distributed in the hope that it will be useful,
17 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
18 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 #  GNU General Public License for more details.
20 #
21 #  The following additional terms are in effect as per Section 7 of the license:
22 #
23 #  The preservation of all legal notices and author attributions in
24 #  the material or in the Appropriate Legal Notices displayed
25 #  by works containing it is required.
26 #
27 #  You should have received a copy of the GNU General Public License
28 #  along with Cnchi; If not, see <http://www.gnu.org/licenses/>.
29
30
31 """ Operations with metalinks """
32
33 import logging
34 import tempfile
35 import os
36
37 import hashlib
38 import re
39 import argparse
40
41 from collections import deque
42
43 from xml.dom.minidom import getDOMImplementation
44 import xml.etree.cElementTree as elementTree
45
46 import pyalpm
47
48 MAX_URLS = 15
49
50
51 def get_info(metalink):
52     """ Reads metalink xml info and returns it """
53
54     # tag = "{urn:ietf:params:xml:ns:metalink}"
55
56     temp_file = tempfile.NamedTemporaryFile(delete=False)
57     temp_file.write(str(metalink).encode('UTF-8'))
58     temp_file.close()
59
60     metalink_info = {}
61     element = {}
62
63     for event, elem in elementTree.iterparse(temp_file.name, events=('start', 'end')):
64         if event == 'start':
65             tag = elem.tag.split('}')[1]
66             if tag == 'file':
67                 element['filename'] = elem.attrib['name']
68             elif tag == 'hash':
69                 hash_type = elem.attrib['type']
70                 try:
71                     element['hash'][hash_type] = elem.text
72                 except KeyError:
73                     element['hash'] = {hash_type: elem.text}
74             elif tag == 'url':
75                 try:
76                     element['urls'].append(elem.text)
77                 except KeyError:
78                     element['urls'] = [elem.text]
79             else:
80                 element[tag] = elem.text
81         if event == 'end' and elem.tag.endswith('file'):
82             # Limit to MAX_URLS for each file
83             if len(element['urls']) > MAX_URLS:
84                 element['urls'] = element['urls'][:MAX_URLS]
85             key = element['identity']
86             metalink_info[key] = element.copy()
87             element.clear()
88             elem.clear()
89
90     if os.path.exists(temp_file.name):
91         os.remove(temp_file.name)
92
93     return metalink_info
94
95
96 def create(alpm, package_name, pacman_conf_file):
97     """ Creates a metalink to download package_name and its dependencies """
98
99     options = ["--conf", pacman_conf_file, "--noconfirm", "--all-deps"]
100
101     if package_name == "databases":
102         options.append("--refresh")
103     else:
104         options.append(package_name)
105
106     download_queue, not_found, missing_deps = build_download_queue(
107         alpm, args=options)
108
109     if not_found:
110         not_found = sorted(not_found)
111         msg = "Can't find these packages: " + ' '.join(not_found)
112         logging.error(msg)
113         return None
114
115     if missing_deps:
116         missing_deps = sorted(missing_deps)
117         msg = "Can't resolve these dependencies: " + ' '.join(missing_deps)
118         logging.error(msg)
119         return None
120
121     if download_queue:
122         metalink = download_queue_to_metalink(download_queue)
123         return metalink
124
125     logging.error("Unable to create download queue for package %s", package_name)
126     return None
127
128 # From here comes modified code from pm2ml
129 # pm2ml is Copyright (C) 2012-2013 Xyne
130 # More info: http://xyne.archlinux.ca/projects/pm2ml
131
132 def download_queue_to_metalink(download_queue):
133     """ Converts a download_queue object to a metalink """
134     metalink = Metalink()
135
136     for database, sigs in download_queue.dbs:
137         metalink.add_db(database, sigs)
138
139     for pkg, urls, sigs in download_queue.sync_pkgs:
140         metalink.add_sync_pkg(pkg, urls, sigs)
141
142     return metalink
143
144
145 class Metalink():
146     """ Metalink class """
147
148     def __init__(self):
149         self.doc = getDOMImplementation().createDocument(
150             None, "metalink", None)
151         self.doc.documentElement.setAttribute(
152             'xmlns', "urn:ietf:params:xml:ns:metalink")
153         self.files = self.doc.documentElement
154
155     # def __del__(self):
156     #    self.doc.unlink()
157
158     def __str__(self):
159         """ Get a string representation of a metalink """
160         return re.sub(
161             r'(?<=>)\n\s*([^\s<].*?)\s*\n\s*',
162             r'\1',
163             self.doc.toprettyxml(indent=' ')
164         )
165
166     def add_urls(self, element, urls):
167         """Add URL elements to the given element."""
168         for url in urls:
169             url_tag = self.doc.createElement('url')
170             element.appendChild(url_tag)
171             url_val = self.doc.createTextNode(url)
172             url_tag.appendChild(url_val)
173
174     def add_sync_pkg(self, pkg, urls, sigs=False):
175         """Add a sync db package."""
176         file_ = self.doc.createElement("file")
177         file_.setAttribute("name", pkg.filename)
178         self.files.appendChild(file_)
179         for tag, db_attr, attrs in (
180                 ('identity', 'name', ()),
181                 ('size', 'size', ()),
182                 ('version', 'version', ()),
183                 ('description', 'desc', ()),
184                 ('hash', 'sha256sum', (('type', 'sha256'),)),
185                 ('hash', 'md5sum', (('type', 'md5'),))):
186             tag = self.doc.createElement(tag)
187             file_.appendChild(tag)
188             val = self.doc.createTextNode(str(getattr(pkg, db_attr)))
189             tag.appendChild(val)
190             for key, val in attrs:
191                 tag.setAttribute(key, val)
192         urls = list(urls)
193         self.add_urls(file_, urls)
194         if sigs:
195             self.add_file(pkg.filename + '.sig', (u + '.sig' for u in urls))
196
197     def add_file(self, name, urls):
198         """Add a signature file."""
199         file_ = self.doc.createElement("file")
200         file_.setAttribute("name", name)
201         self.files.appendChild(file_)
202         self.add_urls(file_, urls)
203
204     def add_db(self, database, sigs=False):
205         """Add a sync db."""
206         file_ = self.doc.createElement("file")
207         name = database.name + '.db'
208         file_.setAttribute("name", name)
209         self.files.appendChild(file_)
210         urls = list(os.path.join(url, database.name + '.db')
211                     for url in database.servers)
212         self.add_urls(file_, urls)
213         if sigs:
214             self.add_file(name + '.sig', (u + '.sig' for u in urls))
215
216
217 class PkgSet():
218     """ Represents a set of packages """
219
220     def __init__(self, pkgs=None):
221         """ Init our internal self.pkgs dict with all given packages in pkgs """
222
223         self.pkgs = dict()
224         if pkgs:
225             for pkg in pkgs:
226                 self.pkgs[pkg.name] = pkg
227
228     def __repr__(self):
229         return 'PkgSet({0})'.format(repr(self.pkgs))
230
231     def add(self, pkg):
232         """ Adds package info to the set """
233         self.pkgs[pkg.name] = pkg
234
235     def __and__(self, other):
236         new = PkgSet(set(self.pkgs.values()) & set(other.pkgs.values()))
237         return new
238
239     def __iand__(self, other):
240         self.pkgs = self.__and__(other).pkgs
241         return self
242
243     def __or__(self, other):
244         copy = PkgSet(list(self.pkgs.values()))
245         return copy.__ior__(other)
246
247     def __ior__(self, other):
248         self.pkgs.update(other.pkgs)
249         return self
250
251     def __contains__(self, pkg):
252         return pkg.name in self.pkgs
253
254     def __iter__(self):
255         for value in self.pkgs.values():
256             yield value
257
258     def __len__(self):
259         return len(self.pkgs)
260
261
262 class DownloadQueue():
263     """ Represents a download queue """
264
265     def __init__(self):
266         self.dbs = list()
267         self.sync_pkgs = list()
268
269     def __bool__(self):
270         return bool(self.dbs or self.sync_pkgs)
271
272     def __nonzero__(self):
273         return self.dbs or self.sync_pkgs
274
275     def add_db(self, database, sigs=False):
276         """ Adds db info and signatures to the queue """
277         self.dbs.append((database, sigs))
278
279     def add_sync_pkg(self, pkg, urls, sigs=False):
280         """ Adds package and its urls to the queue """
281         self.sync_pkgs.append((pkg, urls, sigs))
282
283
284 def parse_args(args):
285     """ Parse arguments to build_download_queue function
286         These arguments mimic pacman ones """
287     parser = argparse.ArgumentParser()
288
289     parser.add_argument('pkgs', nargs='*', default=[], metavar='<pkgname>',
290                         help='Packages or groups to download.')
291     parser.add_argument('--all-deps', action='store_true', dest='alldeps',
292                         help='Include all dependencies even if they are already installed.')
293     parser.add_argument('-c', '--conf', metavar='<path>', default='/etc/pacman.conf', dest='conf',
294                         help='Use a different pacman.conf file.')
295     parser.add_argument('--noconfirm', action='store_true', dest='noconfirm',
296                         help='Suppress user prompts.')
297     parser.add_argument('-d', '--nodeps', action='store_true', dest='nodeps',
298                         help='Skip dependencies.')
299     parser.add_argument('--needed', action='store_true', dest='needed',
300                         help='Skip packages if they already exist in the cache.')
301     help_msg = '''Include signature files for repos with optional and required SigLevels.
302         Pass this flag twice to attempt to download signature for all databases and packages.'''
303     parser.add_argument('-s', '--sigs', action='count', default=0, dest='sigs',
304                         help=help_msg)
305     parser.add_argument('-y', '--databases', '--refresh', action='store_true', dest='db',
306                         help='Download databases.')
307
308     return parser.parse_args(args)
309
310
311 def get_antergos_repo_pkgs(alpm_handle):
312     """ Returns pkgs from Antergos groups (mate, mate-extra) and
313         the antergos db info """
314
315     antdb = None
316     for database in alpm_handle.get_syncdbs():
317         if database.name == 'Reborn-OS':
318             antdb = database
319             break
320
321     if not antdb:
322         logging.error("Cannot sync Antergos repository database!")
323         return {}, None
324
325     group_names = ['mate', 'mate-extra']
326     groups = []
327     for group_name in group_names:
328         group = antdb.read_grp(group_name)
329         if not group:
330             # Group does not exist
331             group = ['None', []]
332         groups.append(group)
333
334     repo_pkgs = {
335         pkg for group in groups
336         for pkg in group[1] if group}
337
338     return repo_pkgs, antdb
339
340
341 def resolve_deps(alpm_handle, other, alldeps):
342     """ Resolve dependencies """
343     missing_deps = []
344     queue = deque(other)
345     local_cache = alpm_handle.get_localdb().pkgcache
346     syncdbs = alpm_handle.get_syncdbs()
347     seen = set(queue)
348     while queue:
349         pkg = queue.popleft()
350         for dep in pkg.depends:
351             if pyalpm.find_satisfier(local_cache, dep) is None or alldeps:
352                 for database in syncdbs:
353                     prov = pyalpm.find_satisfier(database.pkgcache, dep)
354                     if prov:
355                         other.add(prov)
356                         if prov.name not in seen:
357                             seen.add(prov.name)
358                             queue.append(prov)
359                         break
360                 else:
361                     missing_deps.append(dep)
362     return other, missing_deps
363
364
365 def create_package_set(requested, ant_repo_pkgs, antdb, alpm_handle):
366     """ Create package set from requested set """
367
368     found = set()
369     other = PkgSet()
370
371     for pkg in requested:
372         for database in alpm_handle.get_syncdbs():
373             # if pkg is in antergos repo, fetch it from it (instead of another repo)
374             # pkg should be sourced from the antergos repo only.
375             if antdb and pkg in ant_repo_pkgs and database.name != 'Reborn-OS':
376                 database = antdb
377
378             syncpkg = database.get_pkg(pkg)
379
380             if syncpkg:
381                 other.add(syncpkg)
382                 break
383             else:
384                 syncgrp = database.read_grp(pkg)
385                 if syncgrp:
386                     found.add(pkg)
387                     #other_grp |= PkgSet(syncgrp[1])
388                     other |= PkgSet(syncgrp[1])
389                     break
390
391     return found, other
392
393 def build_download_queue(alpm, args=None):
394     """ Function to build a download queue.
395         Needs a pkgname in args """
396
397     pargs = parse_args(args)
398
399     requested = set(pargs.pkgs)
400
401     handle = alpm.get_handle()
402     conf = alpm.get_config()
403
404     missing_deps = list()
405
406     ant_repo_pkgs, antdb = get_antergos_repo_pkgs(handle)
407
408     if not antdb:
409         logging.error("Cannot load antergos repository database")
410         return None, None, None
411
412     found, other = create_package_set(requested, ant_repo_pkgs, antdb, handle)
413
414     # foreign_names = requested - set(x.name for x in other)
415
416     # Resolve dependencies.
417     if other and not pargs.nodeps:
418         other, missing_deps = resolve_deps(handle, other, pargs.alldeps)
419
420     found |= set(other.pkgs)
421     not_found = requested - found
422     if pargs.needed:
423         other = PkgSet(list(check_cache(conf, other)))
424
425     # Create download queue
426     download_queue = DownloadQueue()
427
428     # Add databases (and their signature)
429     if pargs.db:
430         for database in handle.get_syncdbs():
431             try:
432                 siglevel = conf[database.name]['SigLevel'].split()[0]
433             except KeyError:
434                 siglevel = None
435             download_sig = needs_sig(siglevel, pargs.sigs, 'Database')
436             download_queue.add_db(database, download_sig)
437
438     # Add packages (pkg, url, signature)
439     for pkg in other:
440         try:
441             siglevel = conf[pkg.db.name]['SigLevel'].split()[0]
442         except KeyError:
443             siglevel = None
444         download_sig = needs_sig(siglevel, pargs.sigs, 'Package')
445
446         urls = []
447         server_urls = list(pkg.db.servers)
448         for server_url in server_urls:
449             url = os.path.join(server_url, pkg.filename)
450             urls.append(url)
451
452         # Limit to MAX_URLS url
453         while len(urls) > MAX_URLS:
454             urls.pop()
455
456         download_queue.add_sync_pkg(pkg, urls, download_sig)
457
458     return download_queue, not_found, missing_deps
459
460
461 def get_checksum(path, typ):
462     """ Returns checksum of a file """
463     new_hash = hashlib.new(typ)
464     block_size = new_hash.block_size
465     try:
466         with open(path, 'rb') as myfile:
467             buf = myfile.read(block_size)
468             while buf:
469                 new_hash.update(buf)
470                 buf = myfile.read(block_size)
471         return new_hash.hexdigest()
472     except FileNotFoundError:
473         return -1
474     except IOError as io_error:
475         logging.error(io_error)
476
477
478 def check_cache(conf, pkgs):
479     """ Checks package checksum in cache """
480     for pkg in pkgs:
481         for cache in conf.options['CacheDir']:
482             fpath = os.path.join(cache, pkg.filename)
483             for checksum in ('sha256', 'md5'):
484                 real_checksum = get_checksum(fpath, checksum)
485                 correct_checksum = getattr(pkg, checksum + 'sum')
486                 if real_checksum is None or real_checksum != correct_checksum:
487                     yield pkg
488                     break
489             else:
490                 continue
491             break
492
493
494 def needs_sig(siglevel, insistence, prefix):
495     """ Determines if a signature should be downloaded.
496         The siglevel is the pacman.conf SigLevel for the given repo.
497         The insistence is an integer. Anything below 1 will return false,
498         anything above 1 will return true, and 1 will check if the
499         siglevel is required or optional.
500         The prefix is either "Database" or "Package". """
501
502     if insistence > 1:
503         return True
504     elif insistence == 1 and siglevel:
505         for sl_type in ('Required', 'Optional'):
506             if siglevel == sl_type or siglevel == prefix + sl_type:
507                 return True
508     return False
509
510
511 def test_module():
512     """ Module test function """
513     import gettext
514
515     _ = gettext.gettext
516
517     formatter = logging.Formatter(
518         '[%(asctime)s] [%(module)s] %(levelname)s: %(message)s',
519         "%Y-%m-%d %H:%M:%S.%f")
520     logger = logging.getLogger()
521     logger.setLevel(logging.DEBUG)
522     stream_handler = logging.StreamHandler()
523     stream_handler.setLevel(logging.DEBUG)
524     stream_handler.setFormatter(formatter)
525     logger.addHandler(stream_handler)
526
527     #import gc
528     #import pprint
529     import sys
530     cnchi_path = "/usr/share/cnchi"
531     sys.path.append(cnchi_path)
532     sys.path.append(os.path.join(cnchi_path, "src"))
533     import pacman.pac as pac
534
535     pacman = pac.Pac(
536         conf_path="/etc/pacman.conf",
537         callback_queue=None)
538
539     print("Creating metalink...")
540     meta4 = create(
541         alpm=pacman,
542         #package_name="ipw2200-fw",
543         package_name="base-devel",
544         pacman_conf_file="/etc/pacman.conf")
545     #print(meta4)
546     #print('=' * 20)
547     if meta4:
548         print(get_info(meta4))
549     # print(get_info(meta4)['ipw2200-fw']['urls'])
550
551     pacman.release()
552     del pacman
553
554
555 # Test case
556 if __name__ == '__main__':
557     test_module()