debpartial-mirror/0000755000000000000000000000000011656010336011364 5ustar debpartial-mirror/AUTHORS0000644000000000000000000000031611647020704012434 0ustar Otavio Salvador Nat Budin Free Ekanayaka Vagrant Cascadian Marco Presi Martin Fuzzey debpartial-mirror/debpartial_mirror/0000755000000000000000000000000011656010205015060 5ustar debpartial-mirror/debpartial_mirror/DisplayStatus.py0000644000000000000000000001123411647020704020251 0ustar # debpartial_mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador , Enrico Zini # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import fcntl import os.path import struct import sys import termios class StatusItem: id = 0 def __init__(self): self._properties = None def __getitem__(self, key): if key not in ['id', 'finished', 'current', 'size', 'errored']: raise KeyError("%s is not allowed on StatusItem." % key) if self._properties is None: StatusItem.id += 1 self._properties = {'id' : StatusItem.id, 'finished': False, 'current' : 0, 'size' : 0, 'errored' : ''} return self._properties.get(key) def __setitem__(self, key, value): if key not in ['id', 'finished', 'current', 'size', 'errored']: raise KeyError("%s is not allowed on StatusItem." % key) if self._properties is None: StatusItem.id += 1 self._properties = {'id' : StatusItem.id, 'finished': False, 'current' : 0, 'size' : 0, 'errored' : ''} self._properties[key] = value class BaseDisplayStatus: _items = {} def __getitem__ (self, key): return self._items.get(key, None) def start(self, url, size): self._items[url] = StatusItem() self._items[url]['size'] = size self._update() def update(self, url, current): self._items[url]['current'] = current self._update() def errored(self, url, message): if not self._items.has_key(url): self._items[url] = StatusItem() self._items[url]['errored'] = message self._update() def _update(self): pass def clean(self): for i in self._items.keys(): self._items.pop(i) class TextDisplayStatus(BaseDisplayStatus): def __print_status(self): self.__clear_line() for url in self._items.keys(): if self._items[url]['finished'] or self._items[url]['errored']: continue try: sys.stdout.write("[%d %s: %.2f%%]" % (self._items[url]['id'], os.path.basename(url).split('_')[0], self._items[url]['current']*100/ (self._items[url]['size']))) except ZeroDivisionError: pass sys.stdout.flush() def __clear_line(self): if sys.stdout.isatty(): sw = struct.unpack( "hhhh", fcntl.ioctl(1, termios.TIOCGWINSZ, " "*8) )[1] sys.stdout.write("\r" + " " * sw + "\r") def start(self, url, size): BaseDisplayStatus.start(self, url, size) self.__clear_line() sys.stdout.write("\rGetting %d: %s\n" % (self._items[url]['id'], url)) sys.stdout.flush() def update(self, url, current): BaseDisplayStatus.update(self, url, current) self.__clear_line() if current >= self._items[url]['size'] and \ not self._items[url]['finished']: sys.stdout.write("\rDone: %s\n" % url) self._items[url]['finished'] = True self.__print_status() def errored(self, url, message): if not self._items.has_key(url) or not self._items[url]['errored']: BaseDisplayStatus.errored(self, url, message) self.__clear_line() sys.stdout.write("\rFailed: %s %s\n" % (url, message)) sys.stdout.flush() class LogDisplayStatus(BaseDisplayStatus): def start(self, url, size): BaseDisplayStatus.start(self, url, size) print "Getting %d: %s" % (self._items[url]['id'], url) def update(self, url, current): BaseDisplayStatus.update(self, url, current) if current >= self._items[url]['size'] and \ not self._items[url]['finished']: self._items[url]['finished'] = True def errored(self, url, message): if not self._items.has_key(url) or not self._items[url]['errored']: BaseDisplayStatus.errored(self, url, message) print "Failed: %s %s\n" % (url, message) class QuietDisplayStatus(BaseDisplayStatus): def start(self, url, size): pass def update(self, url, current): pass def errored(self, url, message): if not self._items.has_key(url) or not self._items[url]['errored']: BaseDisplayStatus.errored(self, url, message) print "Failed: %s %s\n" % (url, message) class GtkDisplayStatus(BaseDisplayStatus): #Same as TextDisplayStatus, but send data to a widget instead of print pass debpartial-mirror/debpartial_mirror/Dists.py0000644000000000000000000003564611647020704016543 0ustar # debpartial_mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador , Marco Presi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: # Add a control on md5sum to check wich files has to be updated import os import re import time import apt_pkg from cdd import Package from cdd import PackageList from cdd import FileSystem from debpartial_mirror import Config from debpartial_mirror import Download from debpartial_mirror import Failures class Dists: """ This class provides methods to manage dists on partial-mirrors """ _commandExecutor = os.system # injection point for unittests def __init__ (self, backend): self._backend = backend self._filesystem = FileSystem.FileSystem( backend["mirror_dir"], backend["name"], ) self._files = [] self._already_loaded = False def _get_backends_for_names(self, backend_names): backends = [] # Build our list of PackageLists to be used for resolve_depends for backend in self._backend.backends: if backend._name in backend_names: backends.append(backend) return backends def _resolve_deps_using_backends(self, requiredPackages, backends, architecture): # Resolve our dependencies using pkglists pkglists = [] for backend in backends: pkglists.append(backend.get_full_binary_list(architecture)) try: requiredPackages.resolve_depends(pkglists, fastFail=False) except PackageList.BrokenDependencies, exception: userFailure = Failures.DependencyResolutionFailure(exception.broken) if self._backend.get_config_with_default("standalone", False): raise userFailure print userFailure.format( prefix="WARNING: Can't resolve dependencies:", linePrefix=" " ) class MirrorDists(Dists): def __init__(self, backend): Dists.__init__(self, backend) # Partial package lists per architecture self.__bin = {} self.__source = PackageList.PackageList() # Full package lists per architecture self.__full_bin = {} self.__full_source = PackageList.PackageList() def _fill_files(self): # If we already have it doesn't rerun if self._files: return for component in self._backend["components"]: for dist in self._backend["distributions"]: dist='dists/'+dist for arch in self._backend["architectures"]: arch='binary-'+arch mirror_name = self._backend["mirror_dir"] +\ self._backend["name"] self._files.append("%s/%s/%s/Packages.gz" % ( dist, component, arch )) self._files.append("%s/Release" % dist ) self._files.append("%s/Release.gpg" % dist ) if not component.endswith("/debian-installer"): # debian-installer components don't have Release files self._files.append("%s/%s/%s/Release" % ( dist, component, arch )) if self._backend["get_sources"] and not component.endswith("/debian-installer"): self._files.append("%s/%s/source/Sources.gz" % ( dist, component )) def get_index(self): """ Get only index files (on Debian, this mean: Packages.gz and Sources.gz) """ # TODO: Put checking using Release file when available. files = [] self._fill_files() for file in self._files: if not os.path.basename(file).startswith("Release"): files.append(file.split('.gz')[0]) return files def get_binary_list(self, architecture): return self.__bin.setdefault(architecture, PackageList.PackageList()) def get_source_list(self): return self.__source def get_full_binary_list(self, architecture): return self.__full_bin.setdefault(architecture, PackageList.PackageList()) def get_full_source_list(self): return self.__full_source def load(self): if self._already_loaded: return True for file in self.get_index(): distribution = file.split(os.path.sep)[1] # Choose object type if os.path.basename(file) == 'Packages': pkg = Package.Package architecture = re.match(r".*/binary-(.*)/.*", file).group(1) pkglist = self.get_full_binary_list(architecture) elif os.path.basename(file) == 'Sources': pkg = Package.SourcePackage pkglist = self.__full_source architecture = "source" # Read release file to determine component class ReleaseInfo: def __init__(self, distribution, architecture): self.distribution = distribution self.architecture = architecture self.component = None def parse(self, section): self.component = section["Component"] releaseInfo = ReleaseInfo(distribution, architecture) release_filename = os.path.join( self._filesystem.base(), os.path.dirname(file), 'Release' ) if os.path.exists(release_filename): processTagFile(release_filename, releaseInfo.parse) # Load file on list def addPackage(section): package = pkg(section) package.releaseInfo = releaseInfo name = package['Package'] if name in pkglist: oldver = pkglist[name]['Version'] newver = package['Version'] if apt_pkg.version_compare(oldver, newver) < 1: pkglist.remove(name) pkglist.add(package) else: pkglist.add(package) index_filename = os.path.join(self._filesystem.base(), file) if os.path.exists(index_filename): processTagFile(index_filename, addPackage) else: print "Cannot load mirror '%s' due to missing file '%s'" % ( self._backend["name"], index_filename) return False self._already_loaded = True return True def filter(self): pkgfilter = [] try: pkgfilter = self._backend['filter'] except Config.InvalidOption: pass # to load indexes self.load() # Apply filter or use as final list if pkgfilter: for architecture in self._backend["architectures"]: self.__bin[architecture] = self.get_full_binary_list(architecture).filter(pkgfilter) self.__source = self.__full_source.filter(pkgfilter) else: self.__bin = self.__full_bin self.__source = self.__full_source def resolve(self): backend_names = [self._backend._name] try: backend_names = self._backend['resolve_deps_using'] + backend_names except Config.InvalidOption: pass # Is possible to don't have this option for architecture in self._backend["architectures"]: self._resolve_deps_using_backends( self.get_binary_list(architecture), self._get_backends_for_names(backend_names), architecture ) def process(self): self.filter() self.resolve() def writeIndexFiles(self): indices = _Indices() for architecture in self._backend["architectures"]: for pkg in self.get_binary_list(architecture).values(): indices.addPackage(pkg, self._backend, pkg.releaseInfo.distribution) for dist in self._backend["distributions"]: releaseFile = os.path.join(self._filesystem.base(), "dists", dist, "Release") release = tagFileToSectionMapList(releaseFile)[0] release["Archive"] = release["Suite"] indices.writeFiles( self._filesystem, dist, release, self._backend.get_config_with_default("signature_key", None), self._commandExecutor, ) class RemoteDists (MirrorDists): """ This class provides methods to fill dists dir downloading remote files """ def update (self): """ Get only files that need updates """ self._fill_files() download = Download.Download(name="Dist_" + self._backend["name"]) for file in self._files: self._filesystem.mkdir(os.path.dirname(file)) server = "%s/%s" % (self._backend["server"], file) filename = "%s/%s" % (self._filesystem.base(), file) download.get(server, filename) download.wait_mine() for file in self._files: if not os.path.basename(file).startswith("Release"): try: self._filesystem.uncompress(file) except IOError: return False class LocalDists (MirrorDists): """ This class provides methods to fill dists dir downloading local files """ def update (self): """ Get only files that need updates """ self._fill_files() for server, filename, dirname in self._files: orig, filename = file self._filesystem.mkdir(dirname) os.link (orig.split('file://')[1], filename) class MergeDists (Dists): """ This class provides methods to fill dists dir when merging backends """ def __init__(self, backend): Dists.__init__(self, backend) self.__mirrors = self._get_backends_for_names(self._backend['backends']) def merge(self): indices = _Indices() # Fill package lists for architecture in self._backend["architectures"]: for mirror in self.__mirrors: for pkg in self.get_packages_for_mirror(mirror, architecture).values(): indices.addPackage(pkg, mirror, self._backend['name']) # Write package lists and release files release = {"Archive" : self._backend._name} for key in ['origin', 'label', 'description', 'suite', 'codename', 'version']: fieldname = key[0].upper() + key[1:] if self._backend.has_key(key): release[fieldname] = self._backend[key] else: release[fieldname] = "DebPartialMirror" indices.writeFiles( self._filesystem, self._backend._name, release, self._backend.get_config_with_default("signature_key", None), self._commandExecutor, ) def get_mirrors (self): return self.__mirrors def get_packages_for_mirror(self, mirror, architecture): pkgfilter = [] try: pkgfilter = self._backend['filter_%s' % mirror._name] except Config.InvalidOption: return mirror.get_binary_list(architecture) packages = mirror.get_binary_list(architecture).filter(pkgfilter) self._resolve_deps_using_backends(packages, self.__mirrors, architecture) return packages class _Indices: def __init__(self): self._indexByFilename = {} def addPackage(self, pkg, mirror, dist): dist = "dists/" + dist component = pkg.releaseInfo.component or pkg['Filename'].split("/")[1] if pkg['Filename'].endswith("udeb"): component += "/debian-installer" tmp = pkg['Filename'].split("_") tmp = tmp[len(tmp) -1] arch = tmp.split(".")[0] if pkg['Filename'].endswith("deb"): if arch == "all": for arch in mirror["architectures"]: self._addBinaryPackage(dist, component, arch, pkg) else: self._addBinaryPackage(dist, component, arch, pkg) if pkg['Filename'].endswith(".dsc") or pkg['Filename'].endswith(".gz"): self._addSourcePackage(dist, component, pkg) def writeFiles(self, filesystem, dist, release, signatureKey, commandExecutor): def addUnique(collection, item): if item not in collection: collection.append(item) components = [] architectures = [] files = [] def writeReleaseFields(out, *fieldnames): for fieldname in fieldnames: if fieldname in release: out.write("%s: %s\n" % (fieldname, release[fieldname])) for index in self._indexByFilename.values(): if not filesystem.exists(index.getDirectory()): filesystem.mkdir(index.getDirectory()) if index.filename.endswith("Packages"): addUnique(architectures, index.architecture) # Write Release if index.component.endswith("/debian-installer"): # debian-installer components don't have Release files addUnique(components, "/".join(index.component.split("/")[:-1])) else: addUnique(components, index.component) file = os.path.join(index.getDirectory(), "Release") files.append("/".join(file.split("/")[2:])) #?? out = open(os.path.join(filesystem.base(), file), "w+") try: writeReleaseFields(out, "Archive", "Version") out.write("Component: %s\n" % (index.component)) writeReleaseFields(out, "Origin", "Label") out.write("Architecture: %s\n" % (index.architecture)) finally: out.close() # Write index out = open(os.path.join(filesystem.base(), index.filename), "w+") try: for pkg in sorted(index.packageList.values(), lambda x,y : cmp(x["Package"], y["Package"])): out.write(pkg.dump() + "\n") finally: out.close() filesystem.compress(index.filename) filename = "/".join(index.filename.split("/")[2:]) files.append(filename) files.append(filename + ".gz") out = open(os.path.join(filesystem.base(), "dists/" + dist + "/Release"), "w+") try: writeReleaseFields(out, "Origin", "Label", "Suite", "Version", "Codename") out.write("Date: %s\n" % ( time.strftime("%a, %d %b %Y %H:%M:%S UTC", time.gmtime(time.time())) )) out.write("Architectures: %s\n" % (" ".join(architectures))) out.write("Components: %s\n" % (" ".join(components))) out.write("Description: %s\n" % (release['Description'])) out.write("MD5Sum:\n") for filename in files: fullpath = "dists/%s/%s" % (dist, filename) out.write(" %s %8d %s\n" % ( filesystem.md5sum(fullpath), filesystem.size(fullpath), filename, )) out.write("SHA1:\n") for filename in files: fullpath = "dists/%s/%s" % (dist, filename) out.write(" %s %8d %s\n" % ( filesystem.sha1sum(fullpath), filesystem.size(fullpath), filename, )) finally: out.close() if signatureKey: releaseFile = os.path.join(filesystem.base(), "dists", dist, "Release") keySelector = "" if signatureKey != "default": keySelector = "-u %s" % signatureKey rc = commandExecutor("gpg -abs %s -o - %s > %s.gpg" % ( keySelector, releaseFile, releaseFile )) if rc != 0: raise IOError("gpg command returned %s", rc) def _addBinaryPackage(self, dist, component, architecture, package): filename = "%s/%s/binary-%s/Packages" % (dist, component, architecture) index = self._indexByFilename.setdefault(filename, _Index(filename, component, architecture)) index.addPackage(package) def _addSourcePackage(self, dist, component, package): filename = "%s/%s/source/Packages" % (dist, component) index = self._indexByFilename.setdefault(filename, _Index(filename, component, "source")) index.addPackage(package) class _Index: def __init__(self, filename, component, architecture): self.filename = filename self.component = component self.architecture= architecture self.packageList = PackageList.PackageList() def getDirectory(self): return os.path.dirname(self.filename) def addPackage(self, package): if not self.packageList.has_key(package["Package"]): self.packageList.add(package) def processTagFile(filename, sectionHandler): parse_in = open(filename, "r") try: for section in apt_pkg.TagFile(parse_in): sectionHandler(section) finally: parse_in.close() def tagFileToSectionMapList(filename): sections = [] def handleSection(section): tags = {} for key in section.keys(): if key: tags[key] = section[key] sections.append(tags) processTagFile(filename, handleSection) return sections debpartial-mirror/debpartial_mirror/Download.py0000644000000000000000000002633611656010205017213 0ustar # debpartial_mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import StringIO import threading import Queue import re import os import os.path from stat import * import pycurl from string import split, join from rfc822 import formatdate from errno import EEXIST from debpartial_mirror import DisplayStatus class DownloadQueue(Queue.Queue): """ Implement a Queue without duplicated items. """ counter = 0 def _put(self, item): if item not in self.queue: self.queue.append(item) class DownloadFileHandler: def __init__(self, filename, mode, buffering = 0): self._filename = filename self._mode = mode self._buffering = buffering self._openned = False self.__fp = None def write(self, string): if not self._openned: self.__fp = open(self._filename, self._mode, self._buffering) self._openned = True self.__fp.write(string) def close(self): if self.__fp: self.__fp.close() self.__fp = None self._openned = False class Curl: buggy_curl = False def __init__(self, DisplayStatus, Queue): # Handle bug version of pycurl (versions pior 7.14 are buggy) major, minor, revision = pycurl.version_info()[1].split('.') if (int(major) + 0.01*int(minor) < 7.14) and not Curl.buggy_curl: print "WARNING: Due a bug in libcurl up to 7.13 we can't use resume support!" Curl.buggy_curl = True self._curl = pycurl.Curl() self._curl.setopt(pycurl.FOLLOWLOCATION, 1) self._curl.setopt(pycurl.MAXREDIRS, 5) self._curl.setopt(pycurl.NOSIGNAL, 0) self._curl.setopt(pycurl.CONNECTTIMEOUT, 30) self._curl.setopt(pycurl.PROGRESSFUNCTION, self.progress) self._curl.setopt(pycurl.NOPROGRESS, 0) self._curl.setopt(pycurl.FAILONERROR, 1) self._curl.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_1_0) self._curl.setopt(pycurl.VERBOSE, 0) self._curl.parent = self self._curl.callback = None self._curl.error_callback = None self._curl.filename = None self._fp = None self._startSize = 0 self.DisplayStatus = DisplayStatus self.Queue = Queue def __processDir(self): # in case it's a directory, we process to find all needed # objects and queue all. if self._fp != None and isinstance(self._fp, StringIO.StringIO): self._fp.seek(0) matches = re.compile( '', re.IGNORECASE | re.MULTILINE ).findall(self._fp.read()) for filen in matches: try: os.makedirs(os.path.dirname(self._curl.filename)) except OSError, (errno, msg): if errno != EEXIST: print "ERROR:", msg # (uri, destine, callback, error_callback, no_cache) self.Queue.put(( os.path.dirname(self._curl.url) + '/' + filen, os.path.dirname(self._curl.filename) + '/' + filen, self._curl.callback, self._curl.error_callback, self._curl.no_cache )) def set_target(self, url, filename, callback, error_callback, no_cache): self._curl.setopt(pycurl.URL, url) self._curl.url = url self._curl.callback = callback self._curl.error_callback = error_callback self._curl.filename = filename self._curl.setopt(pycurl.RESUME_FROM, 0) self._curl.no_cache = no_cache if self._fp is not None: try: self._fp.close() del self._fp # don't leave the files open except IOError: self._fp = None extra_header = [] if filename.endswith('/'): self._fp = StringIO.StringIO() else: self._curl.filename += ".partial" # We'll rename it back if is the same file if os.path.exists (filename): extra_header.append("if-modified-since:" + formatdate( os.path.getmtime(filename) )) os.rename (filename, self._curl.filename) if not Curl.buggy_curl and os.path.exists (self._curl.filename): #dowload interrupted, needs resume self._startSize = os.stat(self._curl.filename)[ST_SIZE] self._curl.setopt(pycurl.RESUME_FROM, self._startSize) self._fp = DownloadFileHandler(self._curl.filename, "ab") else : self._fp = DownloadFileHandler(self._curl.filename, "wb") self._curl.setopt(pycurl.WRITEFUNCTION, self._fp.write) if not no_cache: extra_header.append("Pragma:") if extra_header: self._curl.setopt(pycurl.HTTPHEADER, extra_header) def close(self): self.__processDir() self.callback() self._fp = None self._curl.close() def progress(self, download_t, download_d, upload_t, upload_d): #If we doesn't know how much data we will receive, return. if download_t == 0 or download_d == 0: return if self.DisplayStatus[self._curl.url] == None and self._curl.url[-1] != '/': self.DisplayStatus.start(self._curl.url, download_t + self._startSize) if self._curl.url[-1] != '/': self.DisplayStatus.update(self._curl.url, download_d + self._startSize) def callback(self): if not self._curl.filename: return # It hasn't set! #remove trailing .partial after download is completed if not isinstance(self._fp, StringIO.StringIO) and os.path.exists (self._curl.filename): self._fp.close() os.rename (self._curl.filename, split (self._curl.filename,".partial")[0]) else: self.__processDir() #exec user passed callback if self._curl.callback != None: self._curl.callback self._curl.callback = None def error_callback(self): if self._curl.error_callback != None: self._curl.error_callback self._curl.error_callback = None class DownloadFetcher(threading.Thread): def __init__(self, info, downloaders): # Add DisplayStatus object. # TODO: the DisplayStatus type should be set in the configuration file if info == "text": self.DisplayStatus = DisplayStatus.TextDisplayStatus() elif info == "log": self.DisplayStatus = DisplayStatus.LogDisplayStatus() elif info == "quiet": self.DisplayStatus = DisplayStatus.QuietDisplayStatus() else: self.DisplayStatus = info # Create the needed pycurl objects to manage the connections. self._multi = pycurl.CurlMulti() self._multi.handles = [] self._free = Queue.Queue() self.queue = DownloadQueue() for i in range(downloaders): curl = Curl(self.DisplayStatus, self.queue) self._multi.handles.append(curl) self.running = False map(lambda x: self._free.put(x), self._multi.handles) threading.Thread.__init__(self) def start(self): self.running = True threading.Thread.start(self) def run(self): total_handles = 0; while self.running: Download.lock.acquire() while self.queue.qsize() > 0: try: fetcher = self._free.get_nowait() url, filename, callback, error_callback, no_cache = self.queue.get_nowait() fetcher.set_target(url, filename, callback, error_callback, no_cache) self._multi.add_handle(fetcher._curl) total_handles += 1 except Queue.Empty: break Download.lock.release() #If all fetcher are free, we finished our job if self._free.qsize() == len (self._multi.handles): break # Run the internal curl state machine for the multi stack ret = self._multi.select(1.0) if ret == -1: print "Select returned -1" break while 1: ret, num_handles = self._multi.perform() if num_handles < total_handles: #One or more fetcher finished download, we call #respective curl callbacks, and free the fetcher itself" num_failed, ok_list, err_list = self._multi.info_read() for curl in ok_list: curl.parent.callback() self._multi.remove_handle(curl) self._free.put(curl.parent) for curl, errno, errmsg in err_list: self.DisplayStatus.errored(curl.url, errmsg) curl.parent.error_callback() self._multi.remove_handle(curl) self._free.put(curl.parent) #update the number of actual fetcher total_handles = num_handles if ret != pycurl.E_CALL_MULTI_PERFORM: break Download.lock.acquire() while self.queue.qsize()>0: self.queue.get_nowait() Download.lock.release() try: while len(self._multi.handles) > 0: curl = self._multi.handles.pop() try: self._multi.remove_handle(curl._curl) except: pass curl.close() del curl self._multi.close() except: pass self.running = False self.DisplayStatus.clean() class Download: """ A Download queue """ # Fetcher to use d_fetchers_list = {} # Our lock handler lock = threading.Lock() def __init__(self, info="text", max=3, name=None): self.__max = max self.__info = info self.__name = name self.d_fetcher = None if self.__name == None: self.__name = str(self).split(" ")[-1].rstrip(">") if not isinstance(self.__name,str): raise TypeError, "The name passed, was not a string" elif self.__name in Download.d_fetchers_list: raise KeyError, "A Downloaded fetcher with name '%s' alrady exists" % self.__name def set_name (name): """Set a name for the queue""" print "This function should not be called" if not self.__name: self.__name = name def request (self, uri, destine, callback = None, error_callback = None, no_cache = False): """Add an uri to the queue. The uri is copied on the local path destine. Optionally, it is possible to pass two callbacks functions that are executed once download is terminated (callback) or if it failed (error_callback)""" # Create the needed d_fetcher. self.lock.acquire() if self.d_fetcher == None or not self.d_fetcher.running: self.d_fetcher = DownloadFetcher(self.__info, self.__max) self.d_fetcher.setDaemon(True) self.d_fetcher.start() Download.d_fetchers_list[self.__name]= self.d_fetcher self.d_fetcher.queue.put((uri, destine, callback, error_callback, no_cache)) self.lock.release() def get (self, uri, destine, callback = None, error_callback = None, no_cache = False): self.request (uri, destine, callback, error_callback, no_cache) def get_name (self): """Return the name of this queue""" return self.__name def get_names (self): """Return a list the current queues""" return Download.d_fetchers_list.keys() def wait(self, name): """Wait for all files in the queue 'name' to be downloaded """ try: self._wait(Download.d_fetchers_list[name]) self.lock.acquire() Download.d_fetchers_list.pop(name) d_fetcher = None self.lock.release() except KeyError: print "No thread with given name" def _wait(self, d_fetcher): d_fetcher.join(0.5) while d_fetcher and d_fetcher.running: d_fetcher.join(0.5) def wait_all(self): """Wait for all the files in all the queues are downloaded.""" # We need to use timeout to handle signals while 1: try: i = Download.d_fetchers_list.iterkeys() n = i.next() f = Download.d_fetchers_list[n] f.running = False self.wait(n) except StopIteration: break def wait_mine(self): """Wait for all files in this queue to be downloaded.""" self.wait(self.__name) debpartial-mirror/debpartial_mirror/Controller.py0000644000000000000000000000414411647020704017565 0ustar from debpartial_mirror import Backend class Controller: def __init__(self, configuration, mirrorNames): self._mirrors = [] self._merges = [] cnf_mirrors, cnf_merges = configuration.get_backends() def useBackend(backend): if not mirrorNames: return True return backend.section in mirrorNames for b in cnf_mirrors: if useBackend(b): self._mirrors.append(Backend.MirrorBackend(b.section, configuration)) for b in cnf_merges: if useBackend(b): self._merges.append(Backend.MergeBackend(b.section, configuration)) self._commands = { "all" : self.doAll, "update" : self.doUpdate, "upgrade" : self.doUpgrade, "merge" : self.doMerge, "clean" : self.doClean, } def isValidCommand(self, commandName): return commandName in self._commands def executeCommand(self, commandName): self._commands.get(commandName)() def doAll(self): self._update() if self._load(): self._process() self._upgrade() self._merge() self._clean() def doUpdate(self): self._update() def doUpgrade(self): if self._load(): self._process() self._upgrade() def doMerge(self): if self._load(): self._process() self._merge() def doClean(self): if self._load(): self._clean() def _update(self): for b in self._mirrors: if b.has_key('lock') and b['lock']: print "Skipping backend", b._name else: print "Updating backend", b._name b.update() def _load(self): for b in self._mirrors: print "Loading backend", b._name if not b.load(): return False return True def _process(self): for b in self._mirrors: print "Processing backend", b._name b.process() def _upgrade(self): for b in self._mirrors: if b.has_key('lock') and b['lock']: print "Skipping backend", b._name else: print "Upgrading backend", b._name b.upgrade() def _clean(self): for b in self._mirrors + self._merges: if b.has_key('lock') and b['lock']: print "Skipping backend", b._name else: print "Clean backend", b._name b.clean() def _merge(self): for b in self._merges: print "Merging backend", b._name b.merge() debpartial-mirror/debpartial_mirror/ChangeLog0000644000000000000000000000603711647020704016645 0ustar 2005-11-26 Marco Presi * DisplayStatus.py (BaseDisplayStatus.clean): added code to clean properly self._items after download queue is empty. * Download.py (Download.d_fetchers_list): code refactoring. proposal for function renaming: Download.get() -> Download.request() * Pool.py (RemotePool.__init__): * Dists.py (RemoteDists.update): * Files.py (RemoteFiles.upgrade): assegned default name to download queue 2005-11-25 Marco Presi * Download.py (Curl.__init__): fixed detection of pycurl version (it was detecting an old version even if I have installed 7.15.0) 2005-11-24 Otavio Salvador * Download.py (Curl.__init__): detect buggy curl version; (Curl.set_target): avoid to resume when buggy_curl is True; (Curl.set_target): avoid to replace full files; 2005-11-23 Otavio Salvador * Dists.py (MergeDists.__init__): moved code to build __mirrors list allowing get_mirrors to work again. This wasn't working since the need code was include in merge method only and thus breaking any other use of it. * Pool.py (MergePool._calc_need_files): implemented to add suport for merge cleanup. * Backend.py (Backend.clean): moved from MirrorBackend class since we now support cleanup of all kinda of mirrors. 2005-11-23 Marco Presi * Download.py (Download): fixed attribut name: self.__name 2005-11-22 Marco Presi * Download.py: (Download): Added a dictionary containing each downloading thread, indexed by name: in this way the main application, can act on a single thread by name, once a Donwload instance has been called. (Curl.set_target): set RESUME_FROM to 0 each time we set a target (if a file as been previously resumed with the same curl instance, this counter contains arbitrary values..) 2005-11-21 Marco Presi * Download.py (Download:): changed namespace fetcher ---> dFetcher, to avoid confusion with terminology. Removed Download.queue as a global vairable. (DownloadFetcher.__init__): Added self.queue = DonwloadQueue, so that each Pool (or Dists), has its own private queue of files to grab from remote servers. 2005-11-19 Otavio Salvador * Pool.py (MergePool.merge): Fix to avoid to try to link already existent files. Also, I removed the evaluate method since there should only has hard links of files so it's obivious useless. * Download.py (Curl): Fix code to handle non-set filename attribute 2005-11-18 Marco Presi * Download.py (Curl.set_target): comment cleaning. (Curl.__init__): moved self._startSize here, with all other properties for better readability. (Curl.set_target): add code to append .partial suffix to files that are not completely downloaded. (Curl.callback): when files are succesfully downloaded, close them and remove .partial suffix (Curl.close): file closing is done on the callback debpartial-mirror/debpartial_mirror/Failures.py0000644000000000000000000000123511647020704017212 0ustar class MirrorFailure(Exception): pass class DependencyResolutionFailure(MirrorFailure): def __init__(self, badDepsByPackage): self._badDepsByPackage = badDepsByPackage MirrorFailure.__init__(self) def format(self, prefix=None, linePrefix=""): lines = [] if prefix is not None: lines.append(prefix) def renderDep(dep): return "".join(dep) def renderAlternatives(deps): return "|".join([renderDep(x) for x in deps]) for packageName, dependencies in self._badDepsByPackage.iteritems(): lines.append("%s%s needs [%s]" % (linePrefix, packageName, ",".join( [renderAlternatives(x) for x in dependencies] ))) return "\n".join(lines) debpartial-mirror/debpartial_mirror/Files.py0000644000000000000000000000350311647020704016502 0ustar # debpartial_mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # TODO: Build two classes to be used. One for remote files and other # for local files. import os.path from cdd import FileSystem from debpartial_mirror import Download from debpartial_mirror import DisplayStatus class Files: """ This class provides methods to manage normal files of partial-mirrors. It can be used to get files based on regexp and full paths. """ def __init__ (self, backend): self._backend = backend self._files = [] self._filesystem = FileSystem.FileSystem(os.path.join( backend["mirror_dir"], backend["name"] )) def upgrade(self): """ This method is used to implement specific logic. You MUST to implement it. """ raise NotImplementedError class RemoteFiles(Files): def upgrade(self): download = Download.Download(name = "Files_" + self._backend["name"]) if self._backend.has_key('files'): for file in self._backend['files']: download.get( self._backend['server'] + '/' + file, self._backend['mirror_dir'] + self._backend['name'] + '/' + file ) download.wait_mine() debpartial-mirror/debpartial_mirror/Config.py0000644000000000000000000002532611647020704016654 0ustar # debpartial_mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador , Nat Budin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import logging import os.path import ConfigParser import re import string class ConfigException(Exception): def getErrorMessage(self): return "Unknwon configuration error: %s" % self.__class__.__name__ class InternalError(ConfigException): def __init__(self, reason): self.reason = reason def getErrorMessage(self): return "You have found a bug, please report it: %s" % self.reason class InvalidOption(ConfigException): """ Exception called when a invalid option is found in configuration file. """ def __init__(self, section, option, reason): self.section = section self.option = option self.reason = reason def getErrorMessage(self): return "Wrong option [%s] found in [%s] section : %s" % ( self.option, self.section, self.reason, ) class RequiredOptionMissing(ConfigException): """ Exception called when a required option in the config file is not present. """ def __init__(self, section, option): self.section = section self.option = option def getErrorMessage(self): return "Required option [%s] not found in [%s] section." % (self.option, self.section) class InvalidSection(ConfigException): """ Exception called when a invalid section is found in configuration file. """ def __init__(self, section, reason): self.section = section self.reason = reason def getErrorMessage(self): return "Invalid section [%s] : %s" % (self.section, self.reason) class ConfigSection: _required = [] _allowed = [] _options_with_type = {} _allowed_in_filter_field = [ 'subsection', 'priority', 'name', 'exclude-priority', 'exclude-subsection', 'exclude-name', 'include-from', 'exclude-from', 'include-script', 'exclude-script', 'field-.*', 'exclude-field-.*', ] _name = "" def __init__(self, config, section): self.config = config self.section = section self._allowed_in_filter_regexp = re.compile("|".join(self._allowed_in_filter_field)) # fix up self._allowed to include all required options too for item in self._required: if item not in self._allowed: self._allowed.append(item) self.confs = {} for item, value in self.config.items(self.section): value = self.__cast_option(item, value) if item not in self._allowed: rv = self.__parse_variable_options(item) if rv is None: raise InvalidOption(self.section, item, "%s section" % self._name) allowed_key, variable, match = rv if variable == 'BACKEND': if match not in self.config.sections(): raise InvalidOption( self.section, item, "[%s] is not the name of a backend" % match, ) else: raise InternalError("[%s] matches unknown variable [%s]" % (item, variable)) self.confs[item] = value for item in self._required: if not self.confs.has_key(item): rv = self.__parse_variable_options(item) if rv is None: raise RequiredOptionMissing(self.section, item) def __parse_variable_options(self, item): allowedRe = re.compile("(.*)@(.*)@(.*)") for allowed_key in self._allowed: allowedMatch = allowedRe.match(allowed_key) if allowedMatch is None: continue before, variable, after = allowedMatch.groups() itemMatch = re.match("%s(.*)%s" % (before, after), item) if itemMatch is not None: return allowed_key, variable, itemMatch.group(1) return None def __cast_option(self, option, value): if option not in self._options_with_type: try: allowed_key, variable, match = self.__parse_variable_options(option) option = allowed_key except TypeError: pass if option in self._options_with_type: if self._options_with_type[option] == 'list': return value.split() elif self._options_with_type[option] == 'boolean': value = string.lower(value) if value in ('0', 'false', 'no', 'off'): return False elif value in ('1', 'true', 'yes', 'on'): return True else: raise InvalidOption(self.section, option, "'%s' is not a valid boolean value" % value) elif self._options_with_type[option] == 'filter': opts = value.split() ret = {} for opt in opts: key, val = opt.split(':', 1) if not re.match(self._allowed_in_filter_regexp, key): raise InvalidOption(self.section, option, "[%s] is not a filter field" % key) if key in ('include-from', 'exclude-from'): if not os.path.exists(val): raise InvalidOption(self.section, option, "file [%s] doesn't exist" % val) if ret.has_key(key): raise InvalidOption(self.section, option, "[%s] option has repeated entries." % key) ret[key] = val return ret else: return value class ConfigGlobal(ConfigSection): _required = [ 'mirror_dir', 'architectures', 'components', 'distributions', 'get_suggests', 'get_recommends', 'get_provides', 'get_sources', 'get_packages', ] _allowed = [ 'debug', 'standalone', 'display', # WARN: this keyword has not been implemented yet ] _options_with_type = { 'architectures': 'list', 'distributions': 'list', 'get_provides': 'boolean', 'get_recommends': 'boolean', 'get_suggests': 'boolean', 'get_sources': 'boolean', 'get_packages': 'boolean', 'components': 'list', 'standalone' : 'boolean', } _name = "global" class ConfigBackendMirror(ConfigSection): _allowed = [ 'server', 'architectures', 'components', 'distributions', 'filter', 'files', 'get_suggests', 'get_recommends', 'get_provides', 'get_sources', 'get_packages', 'resolve_deps_using', 'lock', 'standalone', 'signature_key', ] _options_with_type = { 'architectures': 'list', 'distributions': 'list', 'filter': 'filter', 'files': 'list', 'get_provides': 'boolean', 'get_recommends': 'boolean', 'get_suggests': 'boolean', 'get_sources': 'boolean', 'get_packages': 'boolean', 'components': 'list', 'resolve_deps_using': 'list', 'lock': 'boolean', 'standalone' : 'boolean', } _name = "mirror backend" class ConfigBackendMerge(ConfigSection): _allowed = [ 'filter_@BACKEND@', 'backends', 'name', 'sources_only', 'origin', 'label', 'suite', 'codename', 'version', 'description', 'signature_key', 'standalone', ] _options_with_type = { 'backends': 'list', 'filter_@BACKEND@': 'filter', 'standalone' : 'boolean', } _name = "merge backend" class Config(ConfigParser.ConfigParser): """ Store the configurations used by our system. """ def __init__(self, filename): ConfigParser.ConfigParser.__init__(self) self.read(filename) self.confs = {} self.section_objs = {} self.backends = {} for section in self.sections(): section_type = self.__get_section_type(section) sectionObj = section_type(self, section) self.section_objs[section] = sectionObj self.confs[section] = sectionObj.confs self.confs[section]['name'] = section for section in self.section_objs.values(): if not isinstance(section, ConfigGlobal): self.backends[section.section] = section # Check backend dependencies for backend in self.backends.keys(): self.check_dependencies(backend) def __get_section_type(self, section): # detect which config type this is if section == 'GLOBAL': return ConfigGlobal elif 'backends' in self.options(section): return ConfigBackendMerge elif 'server' in self.options(section): return ConfigBackendMirror else: raise InvalidSection(section, "Unknown section type") def get_dependencies(self, backend): """ Get the list of backends that the given backend depends on """ if isinstance(self.get_backend(backend), ConfigBackendMirror): keyword = 'resolve_deps_using' elif isinstance(self.get_backend(backend), ConfigBackendMerge): keyword = "backends" else: raise InvalidSection(backend, "Don't know how to resolve dependencies") try: dependencies = self.get_option(keyword, backend) return dependencies except InvalidOption: dependencies = [] return dependencies def check_dependencies(self, backend): """ Checks whether the given beckend depends on valid backend names """ dependencies = self.get_dependencies(backend) for dependency in dependencies: if dependency not in self.backends.keys(): raise InvalidSection(dependency, "missing repository") def get_backends(self): # Sort backends unsorted = self.backends.values() sorted = [] names = [] while not len(unsorted) == 0: backend = unsorted.pop(0) name = backend.section dependencies = self.get_dependencies(name) if len(dependencies) == 0: sorted.append(backend) names.append(name) continue deps_ok = True for dependency in dependencies: if dependency not in names: deps_ok = False break if deps_ok: sorted.append(backend) names.append(backend.section) else: unsorted.append(backend) mirrors = [] merges = [] for backend in sorted: if isinstance(self.get_backend(backend.section), \ ConfigBackendMirror): mirrors.append(backend) elif isinstance(self.get_backend(backend.section), \ ConfigBackendMerge): merges.append(backend) return (mirrors, merges) def get_backend(self, name): if name in self.section_objs: return self.section_objs[name] raise InvalidSection(name, "no such backend") def get_option(self, option, section='GLOBAL'): # specified, fall back to GLOBAL if self.confs[section].has_key(option): return self.confs[section][option] if section != 'GLOBAL': logging.debug("[%s] is not present in section [%s]." \ "Fallback to global section." % (option, section)) try: return self.get_option(option, 'GLOBAL') except InvalidOption, msg: raise InvalidOption(section, option, "not found even in global") except InvalidSection, msg: raise InvalidSection(msg.section) else: raise InvalidOption(section, option, "not found") return self.confs[section][option] def dump(self): for section, options in self.confs.items(): print '\n' + section for item, value in options.items(): print " %s = %s" % (item, self.get_option(item, section)) debpartial-mirror/debpartial_mirror/Backend.py0000644000000000000000000000713711647020704016776 0ustar # debpartial_mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador , Marco Presi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from cdd import FileSystem from debpartial_mirror import Config from debpartial_mirror import Dists from debpartial_mirror import Files from debpartial_mirror import Pool class Backend: """ This class provides methods to create backendss dirs into the partial-mirror """ backends = [] def __init__ (self, name, config): # Store myself on list self.backends.append(self) self._cfg = config self._name = name self._filesystem = FileSystem.FileSystem(self["mirror_dir"]) def __getitem__ (self, key): try: item = self._cfg.get_option(key, self._name) except Config.InvalidOption, msg: raise Config.InvalidOption(self._name, key, "lookup") return item def has_key (self, key): try: self.__getitem__ (key) except: return False else: return True def get_config_with_default(self, key, default): if self.has_key(key): return self[key] return default def get_binary_list (self, architecture): """ Return the partial binList associated to this Backend """ return self._dists.get_binary_list(architecture) def get_source_list (self): """ Return the partial srcList associated to this Backend """ return self._dists.get_source_list() def get_full_binary_list (self, architecture): """ Return the full binList associated to this Backend """ return self._dists.get_full_binary_list(architecture) def get_full_source_list (self): """ Return the full srcList associated to this Backend """ return self._dists.get_full_source_list() def filter(self): return self._dists.filter() def remove (self): """ Remove backend """ self._pool.remove() self._dists.remove() def clean (self): self._pool.clean() class MirrorBackend(Backend): def __init__ (self, name, config): Backend.__init__(self, name, config) self._dists = Dists.RemoteDists(self) self._pool = Pool.RemotePool(self) self._files = Files.RemoteFiles(self) def update (self): if not self._filesystem.exists(self._name): self._filesystem.mkdir(self._name) self._dists.update() def load (self): return self._dists.load() def process (self): self._dists.process() def upgrade (self): self._files.upgrade() self._pool.upgrade() if self.isIndexGenerationRequired(): self._dists.writeIndexFiles() def isIndexGenerationRequired(self): return self._pool.containsForeignPackages() class MergeBackend(Backend): def __init__ (self, name, config): Backend.__init__(self, name, config) self._dists = Dists.MergeDists(self) self._pool = Pool.MergePool(self) self._files = Files.RemoteFiles(self) def merge (self): self._dists.merge() self._pool.merge() def get_mirrors (self): return self._dists.get_mirrors() def get_packages_for_mirror(self, mirror, architecture): return self._dists.get_packages_for_mirror(mirror, architecture) debpartial-mirror/debpartial_mirror/__init__.py0000644000000000000000000000134311647020704017177 0ustar ########################################################################## # # __init__.py: defines this directory as 'debpartial_mirror' package. # # debpartial_mirror is an application to make easier to build full or # partial mirrors from Debian or compatible archives. # See http://partial-mirror.alioth.debian.org/ for more information. # # ==================================================================== # Copyright (c) 2002-2005 OS Systems. All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # ######################################################################### # Author: Otavio Salvador debpartial-mirror/debpartial_mirror/Pool.py0000644000000000000000000001337211647020704016356 0ustar # debpartial_mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador , Marco Presi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import sys import apt_pkg from cdd import FileSystem from debpartial_mirror import Download class Pool: """ This class provides methods to manage pool dirs into the partial-mirror. """ def __init__(self, backend): self._backend = backend self._got_files = [] self._containsForeignPackages = False self._filesystem = FileSystem.FileSystem( backend["mirror_dir"], backend["name"] ) def _process_package(self, pkg): """ This method is called for each package and should be override in each child class. """ raise NotImplemented def _calc_need_files(self): """ This method is called to build the list of need files to permit to us to clean up the mirror. If you need to handle in a special way to calc it, you're free to override this method. """ if not len(self._got_files): self._backend.process() need_files = [] for architecture in self._backend["architectures"]: need_files.extend([pkg['Filename'] for pkg in self._backend.get_binary_list(architecture).values()]) else: need_files = self._got_files return need_files def _link_internal(self, pkg, mirror): filename = pkg['Filename'] self._filesystem.mkdir(os.path.dirname(filename)) src = self._backend["mirror_dir"] + "/" + mirror["name"] + "/" + filename dst = os.path.join(self._filesystem.base(), filename) if not os.path.exists(dst): try: print "Linking [%s] %s" % (mirror["name"], filename) os.link (src, dst) except OSError, msg: print "Error while linking a file:" print " [%s] %s" % (mirror["name"], filename) print " System message: %s" % msg sys.exit(1) #TODO: fixme self._got_files.append(filename) def upgrade(self): """ Manage the pool upgrade process TODO: Handle source packages. """ def processPackage(pkg, architecture): foundList = self._backend.get_binary_list(architecture).whereis(pkg["Package"]) if foundList in (self._backend.get_binary_list(architecture), self._backend.get_full_binary_list(architecture)): self._process_package(pkg) else: # required dependency from another mirror for backend in self._backend.backends: if backend.get_full_binary_list(architecture) == foundList: break else: raise RuntimeError("No backend found for %s", pkg["Package"]) self._link_internal(pkg, backend) self._containsForeignPackages = True for architecture in self._backend["architectures"]: for pkg in self._backend.get_binary_list(architecture).values(): filename = pkg['Filename'] if not self._filesystem.exists(filename): processPackage(pkg, architecture) elif self._filesystem.md5sum(filename) == pkg['MD5sum']: self._got_files.append(filename) else: print 'Removing corrupted package', pkg['Package'] self._filesystem.remove(filename) processPackage(pkg, architecture) def clean (self): """ Clean old files """ need_files = self._calc_need_files() # Walk in all debian related files of mirror for current in self._filesystem.match("^.+\.(dsc|(diff|tar)\.gz|deb|udeb)$"): if current not in need_files: print "Removing unneeded file: %s" % current self._filesystem.remove(current) try: self._filesystem.rmdir(os.path.dirname(current), True) except OSError: pass def containsForeignPackages(self): return self._containsForeignPackages class RemotePool (Pool): """ This class provides methods to fill pool dir downloading remote files """ def __init__ (self, backend): Pool.__init__(self, backend) # Call parent constructor self.download = Download.Download(name = "Pool_" + backend["name"]) # Our Download manager def _process_package (self, pkg): self._filesystem.mkdir(os.path.dirname(pkg['Filename'])) self.download.get( os.path.join(self._backend["server"], pkg['Filename']), os.path.join(self._filesystem.base(), pkg['Filename']) ) self._got_files.append(pkg['Filename']) def upgrade (self): Pool.upgrade(self) # Call parent method self.download.wait_mine() # Wait until our downloads finish class LocalPool (Pool): """ This class provides methods to fill pool dir linking from local files """ def _process_package (self): self._filesystem.mkdir(os.path.dirname(pkg['Filename'])) os.link( self._backend["server"].split('file://')[1] + pkg, self._local + pkg ) self._got_files.append(pkg['Filename']) class MergePool (Pool): """ This class provides methods to fill pool dir mergin other pools """ def _calc_need_files(self): need_files = [] for architecture in self._backend["architectures"]: for mirror in self._backend.get_mirrors(): for pkg in self._backend.get_packages_for_mirror(mirror, architecture).values(): need_files.append(pkg['Filename']) return need_files def merge (self): for architecture in self._backend["architectures"]: for mirror in self._backend.get_mirrors(): for pkg in self._backend.get_packages_for_mirror(mirror, architecture).values(): self._link_internal(pkg, mirror) debpartial-mirror/COPYING0000644000000000000000000004311011647020704012416 0ustar GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. debpartial-mirror/debpartial-mirror0000755000000000000000000001045111647021346014735 0ustar #!/usr/bin/env python """ Tool to build partial debian mirrors (and more) """ # debpartial-mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # ------- # Imports # ------- import getopt import os import signal import sys from debpartial_mirror import Config from debpartial_mirror import Controller from debpartial_mirror import Download # --------- # Variables # --------- # Defaults version_str = '0.3.0' conffile = "/etc/debpartial-mirror.conf" # User command descriptions cmnds_desc = { 'all': 'Update, upgrade, merge the selected mirror(s)', 'update': 'Update selected mirror(s) (download Package and Source lists)', 'upgrade': 'Upgrade selected mirror(s) (download binary and source packages)', 'merge': 'Merge selected mirror(s)', 'clean': 'Clean selected mirror(s)', } # Sorted list of user commands cmnds_list = cmnds_desc.keys() cmnds_list.sort() # --------- # Functions # --------- def version(): """Print package version""" print """debpartial-mirror %s - Partial mirroring tool for Debian This program is free software and was released under the terms of the GNU General Public License """ % (version_str) def usage(ret=2): """Print program usage message""" global conffile global cmnds_desc global cmnds_list cmnds_string = "" for c in cmnds_list: cmnds_string += " %-27s %s\n" % (c, cmnds_desc[c]) print """Usage: debpartial-mirror [OPTIONS] COMMAND [MIRROR ..] Where OPTIONS is one of: -h, --help Display this help end exit -c, --configfile=FILE Select a config file (currently '%s') -v, --version Show program version COMMAND is one of: %s And MIRROR selects which mirrors we should work with (all by default). """ % (conffile, cmnds_string) sys.exit(ret) def sigint_handler(signum, frame): d = Download.Download() if len (d.d_fetchers_list) > 0: d.wait_all() print "\n\rInterrupting download due a user request ..." sys.exit(1) def main(): """Main program""" global conffile global cmnds_list global mirrors global merges cmnd = None # Parse options try: opts, args = getopt.getopt(sys.argv[1:], 'hvdc:', ["help", "version", "debug", "configfile="]) except getopt.GetoptError: print "ERROR reading program options\n" usage() for o, v in opts: if o in ("-h", "--help"): usage() if o in ("-v", "--version"): version() sys.exit(0) if o in ("-d", "--debug"): import logging logger = logging.getLogger() logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler(sys.stderr)) if o in ("-c", "--configfile"): if v == '': usage() conffile = v if not os.path.exists(conffile): print "ERROR: no configuration file %s" % conffile sys.exit(1) if not os.path.isfile(conffile): print "ERROR: %s is not a file" % conffile sys.exit(1) if len(args) > 0: cmnd = args[0] if cmnd not in cmnds_list: print "ERROR: Unknown command '%s'" % cmnd usage() mirrorNames = [] if len(args) > 1: for i in range(1, len(args)): mirrorNames.append(args[i]) # Load configuration file try: cnf = Config.Config(conffile) except Config.ConfigException, ex: print "ERROR: %s in file '%s'." % (ex.getErrorMessage(), conffile) sys.exit(1) # Verify if mirror names valid for mirror in mirrorNames: if not cnf.has_section(mirror): print("Unknown MIRROR [%s] on '%s'." % (mirrorNames, conffile)) sys.exit(1) controller = Controller.Controller(cnf, mirrorNames) if not controller.isValidCommand(cmnd): usage() controller.executeCommand(cmnd) # ------------ # Main Program # ------------ if __name__ == '__main__': signal.signal(signal.SIGINT, sigint_handler) main() debpartial-mirror/setup.py0000644000000000000000000000235011647021346013101 0ustar ########################################################################## # # setup.py: File to build and install the application. # # debpartial-mirror is an application to make easier to build full or # partial mirrors from Debian or compatible archives. # See http://partial-mirror.alioth.debian.org/ for more information. # # ==================================================================== # Copyright (c) 2002-2005 OS Systems. All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # ######################################################################### # Author: Otavio Salvador from os import listdir from distutils.core import setup lib_files = [] for f in listdir('debpartial_mirror/'): if f[-3:] == '.py': lib_files.append(f) setup( name = "debpartial-mirror", version = '0.3.0', author = "Otavio Salvador", author_email = "otavio@debian.org", url = "http://partial-mirror.alioth.debian.org", license = "GPL", description = "tools to create partial Debian mirrors", scripts = [ 'debpartial-mirror' ], packages = [ 'debpartial_mirror' ] ) debpartial-mirror/doc/0000755000000000000000000000000011647020704012131 5ustar debpartial-mirror/doc/README0000644000000000000000000002250011647020704013010 0ustar Introduction ============ Debpartial-mirror is a tool used to perform manipulations on debian repositories generating other repositories. The manipulations are : * Pruning : creating a subset of a repository based on file structure: - architecture (eg i386, amd64, ...) - distributions (eg etch, lenny, ...) - components (eg main, contrib, ...) * Filtering : selecting a subset of packages based on various criteria: - package name - priority (eg optional, extra, ...) - subsection (eg gnome, kde, games, ...) - arbitary package fields (tags, ...) - debian-cd taskfiles * Dependency resolution * Merging : combining repositories * Signing The input repositories can be local or remote (via http(s), ftp(s)) They must be "automatic" ie have the structure dists/dist-1/Release /component1/binary-arch1/Packages /Packages.gz /Release The follwing variants are supported * Package pools * Non pooled repositories (such as those created by debarchiver) * Signed repositories (Release.gpg file) For the moment the following are NOT supported (planned for next release) * Trivial (flat) repositories * Source packages As debpartial-mirror only works on repositories if you have some locally built .deb files you will need some other tool to make them into a repository that can be processed by debpartial-mirror. Packages to look at for this include: * debarchiver * reprepro Configuration file structure ============================ The default configuration file is /etc/debpartial-mirror.conf however other files may be used with the -c command line argument. The configuration file has the syntax: [GLOBAL] global configration... [some repository] configuration for some repository ... [another repository] configuration for another repository IE a global section and one section per repository. The global section MUST contain the following keys: mirror_dir base directory for the generated repositories each repository is stored as a subdirectory under here architectures space seperated list of architectures to keep (pruning) components space seperated list of components to keep (pruning) distributions space seperated list of distributions to keep (pruning) get_suggests get_recommends get_provides get_sources, get_packages true / false (NOT YET IMPLEMENTED) In addition it MAY contain the following keys: standalone true => error will be raised if all dependencies are not resolved false (default) => allow repositories with unresolved dependencies debug true / false All of these keys (except debug) may be overridden on a per repository basis. Input repositories ================== A remote repository is specified by a http or ftp URL pointing to the repository root (where the dists directory lives): [debian] server = http://ftp.debian.org/debian A local repository is specified using a file URL: [my-stuff] server = file:///var/lib/custom-packages Note that even for local repositories the (possibly pruned or filtered) files are COPIED into the directory managed by debpartial-mirror (in contrast to merging _between_ repositories already managed by debpartial-mirror where links are used). This is because the file URL could possibly point to removable storage etc. An option to enable links for local repositories may be added in the future to avoid wasting disk space where the input repository is permenant. Pruning ======= This is done using the architectures, distributions, components keys (either in the GLOBAL section or the per repository section) : [etch] server = http://ftp.debian.org/debian architectures = i386 amd64 distributions = etch componnets = main There will be no Pacakges files for the excluded architectures, distributions and componants. The master Release file will however still list the excluded parts (since it is signed and thus cannot be modified) Filtering ========= Filtering is done using the "filter" key in a repository section and alows the set of included packages to be reduced. Filter takes a space seperated list of filter-type:filter-value pairs. Where multiple pairs are specified the resulting package list is the logical OR of the packages accepted by each pair. The allowed filter-types and the meaning of the associated filter-value are: name regular expression matching a package name (from the start of the name) Eg name:linux will match linux-2.6, linux-image but NOT syslinux wheras name:.*linux will match all three packages above See the python documentation for details of the regular expression syntax subsection regular expression matching a package subsection (eg games, kde, ...) priority regular expression matching a package priority (eg optional, extra, ...) exclude-name, exclude-subsection, exclude-priority as above except that the matched packages are excluded include-from a file in the debian-cd task list format listing packages to include exclude-from a file in the debian-cd task list format listing packages to exclude field-XXX regular expression matching a field in the package header. Most useful XXX is probably "Tag". The package is included if a match occurs. Note that, unlike name, subsection and priority indexes are not used so this may be quite inefficient for large repositories. exclude-field-XXX regular expression matching a field in the package header. Most useful XXX is probably "Tag". The package is excluded if a match occurs. Example [my-repository] filter = exclude-section:games|kde exclude-field-Tag:.*implemented-in::ruby (No I don't have anything against ruby or kde really!) A filtered repository only contains a subset of the original package files but FULL index files (Packages, Package.gz etc) for all included architectures, components and distributions. This is done to avoid breaking signatures but does mean that a package search on such a repository will list packages that have been removed (an attempt to install them will thus fail). A future release may include an option to force index regeneration if desired (and optionally resign with another key). Dependency resolution ===================== An attempt will always be made to resolve dependencies within a repository. This means that even if a package would normally be excluded by a filter it will still be included if it is required as a dependency of another package accepted by the filter. For example [my-repository] filter = name:apache exclude-name:perl Will *include* the apache package and its dependencies (of which perl is one) Currently only *hard* dependencies (ie the Depends: field in the package control file) are considered. A later release will implement the get_suggests and get_recommends configuration keys to extend this to softer dependencies. If dependency resolution fails the result depends on the "standalone" configuration key. If this is false (or not specified) a warning message is printed but processing continues. This is reasonable since the repository may well be an "extension" to be used in conjunction with other repostories (typically the official Debian ones). On the other hand if standalone = true an error occurs if all dependencies cannot be resolved. It is also possible to specify other repositories to use for dependency resolution. For example [etch] ... [my-stuff] server = file:///var/lib/custom-packages filter = name:my-thing resolve_deps_using = etch standalone = true Will take the package my-thing from the custom-packages local repository AND all its dependencies, including those in etch to create a standalone repository Note that this requires the index files to be regenerated (since the resulting repository has packages that did not exist in custom-packages). This may have implications for signing. Merging ======= Merging is the process of taking two (or more) repositories and combining them into one. This is done using the "backends" configuration key rather than server [etch-main] server = http://ftp.debian.org/debian distributions = etch components = main [custom] server = file:///var/lib/custom-packages [custom-etch-main] backends = etch-main custom Here custom-etch-main will contain all the packages from etch-main and custom This requires index and release file regeneration. The following keys may optionally be used in a merged repository to specify the contents of the release file: origin label suite codename version description It is also possible to filter while merging: [mything-etch-main] backends = etch-main custom filter_custom = name:mything Signing ======= When the release and index files are regenerated, that is : * When merging * When using resolve_deps_using The resulting repository may be signed using the "signature_key" configuration key. This takes either "default" or any value accepted as a key id by gpg : [myRepo] backends = src1 src2 signature_key = 9B2DC6BF [anotherRepo] backends = src3 src4 signature_key = default Obviously the key to be used must be available in the secret keyring of the user running debpartial-mirror. Running debpartial-mirror ========================= Typical invocations: debpartial-mirror all Run all mirror actions using default configuration file debpartial-mirror -c myConfig.conf all Run all mirror actions using a specific configuation file See manpage for more info. debpartial-mirror/doc/IDEAS0000644000000000000000000001227111647020704012704 0ustar ========================= DEBPARTIAL-MIRROR IDEAS ========================= This page is to coordinate `debpartial-mirror`_ development. .. _`debpartial-mirror`: http://packages.debian.org/unstable/net/debpartial-mirror Introduction ============ ``debpartial-mirror`` is a tool to create a local partial mirror of apt-repository. The *partial-mirror* can be used both as a source of packages to build **Custom Debian Distributions** archives, cd-images or local APT repositories. The packages included into the partial mirror can be downloaded from remote APT repository or local directories. The list of packages that are included into the partial-mirror is specified by means of a configuration file: it is also possible to filter from a package list. Mirror organization ------------------- The partial mirror is organized as a collection of Backends. A Backend is a repository with packages from one source (a local or a remote repository): /partial_mirror/ sarge/ sid/ debian-np/ local-np/ br/ it/ .... In turn each backend, has a fixed structure: /partial_mirror/backend/ dists/ pool/ the ``dists/`` directory contains the ``Packages.gz``, ``Sources.gz`` and ``Release`` files for the Backend, while the ``pool/`` directory contais an apt pool-style tree of packages. Merged backends --------------- Once each Backend is filled, it is possible to create several *Merged Backends*. A *Merged Backend* is a Backend built using a list of Backends from the local mirror that contains a ``dists/`` dir that merges the contents of the different ``dists/`` subdirectories of the selected Backends and a ``pool/`` subdirectory built using symbolic (or hard) links to the packages included on the merged Backends. In addition, when the Merged Backend is created, it is possible to filter which packages to include from the selected Backends. Backend snapshots ----------------- Each backend has a list of binary and source packages inside the ``dists/`` subdirectory and a copy of the corresponding files into the ``pool/`` dir. The standard way of updating each backend would be to download the new Packages and Sources lists, remove the old files from the pool and download the new ones. A very interesting option could be to support multiple *snapshots* of the same backend, the idea would be to keep diferent versions of the ``dists/`` subdirectory using a file structure like the following: /partial_mirror/backend/ snapshots/YYYYMMDD/dists/ snapshots/YYYYMMDD+1/dists/ ... dists/ pool/ With this schema each ``dists/`` subdirectory contains the corresponding lists of source and binary packages and the ``pool/`` subdir contains the source and package files of all included snapshots. Of course the system has to allow the selective removal of one snapshot (i.e., remove the 20041109 snapshot), that is, it has to support the removal of the source and package files that belong to the selected snapshot and are not included on any of the rest of *snapshots*. Shared pool ----------- Extending the idea of the *snapshots* presented before we could use a shared ``pool/`` to store the source and package files included on each *backend*. This could reduce the download time and the pool size; if a file is included into two *backends* it is only installed once. The problem with this model is similar to the one described earlier: to remove unwanted files from previous *backends* without breaking all the rest we have to calculate the differences as described when talking about removing a *snapshot*. Program description =================== .. Note: This is different from the actual implementartion, the following could be used as trace for the man-page. SYNOPSIS ........ :: debpartial-mirror [-c=file] {update [backend, [....]] | merge [backend, [....]] | | check | clean [backend, [....]]} DESCRIPTION ........... ``debpartial-mirror`` will accept some command in the way apt-get does, and read conf information from a configuration file (default ``/etc/debpartial-mirror/debpartial-mirror.conf``) that in turn can include other conf files. update this command let debpartial-mirror to update the backends that compose the partial mirror. If no backend is specified, all backends will be update. Else, only the listed backends will be updated. merge this command will create one (or more) merge backend. The list of backends to be used to merge is specified in the main conf-file. After update and/or merge, debpartial-mirror will clean up the mess removing old and uneeded files from it's structure. Notes about the source code =========================== The backend class ----------------- Properties: need_update (boolean) Members: Dists -> A class that realize backend/dists PackageList -> A class that contains the list of packages that should be copied into the partial-mirror of this backend Methods: create-> update -> Sync the partial-mirror with the sources repositories __create_package_list -> Generate a package list reading from main conf-file. The dists class --------------- Properties: got_files Methods: update -> Update the Dists files (Packages, Release and Sources) _get -> Get each file debpartial-mirror/doc/download_issues0000644000000000000000000000263211647020704015261 0ustar Here, there is a summary of what discussed on 30_12_2004 (really late in the nigth!!) about how to implement download of binary packages, source packages, and general extra files (documentation, ....) from remote mirrors. The following is an attempt to resolve the following problems: o) how to clean old or unneeded files (cleanup) o) how to update the mirrors managing disk space (i.e.: remove old unneeded files _before_ download newer files ) o) do all above in a fast way --------------------------------------------------------- Procedure --------------------------------------------------------- * build a list of files contained into the local mirrors * get indices of files to download from each remote repository * build a list of files to be downloaded (and a list of unneede files that shuold be given by the list obtained by difference..) * evaluate needed disk-space and cleanup if needed/wanted * start download ---------------------------------------------------------- Considerations ---------------------------------------------------------- Trusting: Mirror should be trusted. This can be accomplished by tracking md5 infos on index files (moreover those files are signed); a complete verify on md5sums on mirrored packages can be realized when using them (via apt, or when building iso, and so on). Resuming: Download manager should be sligthly modified to create tempfiles for resume operations. debpartial-mirror/doc/KNOWN_ISSUES0000644000000000000000000000035511647020704014046 0ustar if run without any options, it displays the usage info, but also creates a debpartial-mirror directory (maybe from defaults in /etc/debpartial-mirror.conf?). should probably perform just like --help, and not create any new directories. debpartial-mirror/doc/ClassDiagram1.png0000644000000000000000000014225211647020704015260 0ustar ‰PNG  IHDRÁ[Ž)¶(€IDATxÚìÝ[ŽÜ¸²¨á††Ÿjgþ£Xk6{>®½]keg‹dð&‘Ò×ÂNg*u¥˜Š¿‚¡¿þçþç/àÎ8 Á ¸›ûýß/˜È÷ß|ÓA.—`¿~ý†óG‚ýüa:h àÆìãã÷ÿè $€ÛJ°ÿG‚H0$$®ªãô«¥mhO Á$¶W~$$.`¯Ÿ½~ÿ?@‚H0ÜJ‚¥>H0¼”Öòý7ß´ÀµN‚áòL0 $øŠ/þýûVÅŸÀøçgp¬ý®u $H0@àÆñÇ÷”Ú?pîµN‚aŠ{­ùE‚$Î Œw¹§Ôþs¯u CÄÅ»ìJ¿'Á@‚$˜ö`H0 ìÜz2¿flCnÙ­ëëùÞ¬ý$ÁH°;^¿3®C`@‚@‚]@Í lg,»e9©ïD—µcÀO‚=C~\Õ®gö Á`€;U‚•²¸ÞßO½ œß¿ŸZižÈvå¾}¯v}$ v†;«]½Î]§©k÷õ³'eg’`@‚ t^ý;òììáT¥¬š@zô2ìÈû¥ –QûA‚‘`Wˆ°Ùíºu9)æ\’`H0 ØPlÕˆ¯œ«·f{µÙY‘ ¯hVVä³Z±–ËR‰®£6»¬f™$ v¦;£]G¯ÉÈuìü“`H0 Øà†=—¥•ú¼” –ZFî³–í@Ô¦µ™ -s˶”^Ì`iÝŒ[Afn×-"˜#Á€6•_=Ÿ·dÍÜ&$Ø,š­2âõˆmŠÌ7úÉ—$V`‘vM‚=P‚À<>H0àæ™`©ïF%˜L0`ïaÒ»Ô;ÖÔ[h¾T+hÆPÌèv´ í,í F‚]uíÎl×5×% & ²™`$pßš`ï"¬”­u$³Ôî‘9JÔü>óûO ŒI0€#Á``ª #ÆJ,Ç“\ë$5ÁÀ‚Y£$H0ç Á`àöYa$H0ç Á`àö2Œ æœËK°×bµGlkÜ^Gè¯_@‚€ýž4+˜ `y Ö+¯FH°Ôw¢Ë"Î€Õ ãÿ“ÜM³ùÌg>ó™Ï|æÛ>ÁH0ÛH°’{ÏÒJen•æ)-·ôÝè{µë  ´g‚ãùØF‚8ïZŸ.ÁJÃ%#e¥uÖÉ,‰®šm@‚5Á°VvÈNÌ9λÖOËkM¹L­šuÔf—Õ, <$ `Uâ«æuÍ:jÅ[t?` CPLûnY¦‹Ãï•`£Ÿ| €@ö Lû6•`Q”*€_*Œô¤¸hýžå¥²ÍÅH00N~‘ Á´` Öè^ù}$8èc­ØY¤¶ô Áv•`«l«k ’`@‚€{I³ð»ˆ1 v·¶êÚr­/+Á€ÑL°×’&ÿ|2Ø_Ùïäæ«É(I­§´ïóÔI°šö_jã¥6˜js‘kç}žè5äÚ $èY ”‚æ÷×=#QQó$XKûÏÉ©Þ׭לk $``¢+e«DúÚ`}”t Ö*ÁzäTM¦W)C+’©åÚ $” Ö"Z‚f: v V“‘ÕÒŽ][ Á€$˜à•#Á\[ Á`$$X]PÞZ©f½‘ÂÞŠw“`gÆ´ÁRñúQ½þ\[ Á€‰øâž"ÁRí¿÷:pÁµN‚ ÁÀ&™0€á$pK ÖºÜW[~Ö:`€ ¦ý$Øi¬w[H0€$ÆÚ?@‚5I°÷¬¬Òë÷ÿGEUn;z× €×ÖDúøØº&€9×úP vôYNH¥>ëÍâšµN$ ƒõ³Cv’`ÎpÞµ~©ë𨓠F‚$  0ÖþŒ@‚j‚iÿ€š`-¬4qdaü™ë@‚$˜ö(Œÿñ”*DRÑõ\' ì#F‹„–å½ç+FYeŸÆ÷hÿ‘ùGµ3Ú`jÚ?–’`Gà``U V³ž^ ¶¢Ø-,V–2ÁÆ´£^‘{·ö™`@‚€! %X^ÿðž ÒKó§–‘z/2Oê{¹í-íçÑÿ#r¢v™GÇ&µ/¥ãJ‚]— –j+ѶØ+¦®õI숙7éC Ávh¿gýþ]!—H0è÷q†{ÍÖ8ÊÞHet}''°Rˈ,·F–½û%!‘[NnßSÛM‚]Û·¤²¿z%X´-ä®…–ött}ŽË©¾ 4oË{¹ãRºŽ£ËjÙ¾Ü1Èõe$ØE™`$Á F‚å³£[†ý×.»”™VÊúŠfbG¶«´=©}Œngí2¡ßǺ¬6{«5`.­'òºæû£‚ãQÛ*0^C~µJ°Úó_s-DÚñûëQ,*’G -ŽÄÔþ¼þrÂÑ5¿¡Á F‚åÅVêÿï‚§§Vfëw[¤]m¦xÏ>–¶C»ÔïcÏL°ž,Žž—–Û*ÁZ‡¦ÕîQÙ&0¾vèãl V{mô^#$X.­”­Õ[߯&#k¦!¾H0 €`ˆÃ-$XȪ©óÕ:oMÝÎÚuŒÚè÷±WM°ÖL•¡T›u5C‚µ|W‘ì} 㟙 Ö"wz³{¶¿v;FÔ $ÁH0AÁ †I°^Ö›qvÕ6“`ú}`WJ°–:F# F‚í(ÁZ…3 F‚‘`C$,ŒßúÄcŒÓïãi5ÁZ‡ÖœÎ­§f8dm-ŸšÚa-…ñÏ|â v‹#ì‘?3‡CF3À"ûÒZ´p^­Û®0> @0D‚aÉö[ªƒU#½Z†=æ¶©fè㈡–¥ï”2ßj ã©ßÇ}2Áv ¾®ü>î#Á¢¬Ú†Z·ÿN×ë™Á ú}ÜR‚`$Ø•Ã!Ÿšù³Ú>?= ‹Á ú}<$ $˜ö` "Á@¿$˜ö<±Tc+R?U|ÿ×¤ÂøÑe ÁH0Ðïc¬8ÊØx¼Ž±È÷rYYGÙ"‘Œ®Òzs(©uåö3·žšmŒÎ›Û‡Ôg¹cB‚‘`­ç®Ô6#í¹'»±t}åö%"C¢ïåŽKézŒf™Õö ¥þ šYZ³-Ñë5Ò?ž½þË$†H0Ðïc„Èѹu¾Ú@´ç{=Û †ß_÷îO‹´k=$Ø_C²aZDSm{Lµ³^ Â-í)*e¢ûßz=èCZ¯©èvZ? @0D‚€~ÛK°hFVOE‹±¼â¢%€Ždž´fÎD×G‚Ï«m‹³¯ –ËF+ekæÜÒ§ÔdEÕfVµ\Ï-CÏÛ³Ä @0ô` †¾W º'ÿú}ôû¸O&Øè€µW&µ¾«H°YE­2€W¬å¸Ï¾>Z¶¹v;f¯3%ØU•$ @0t+ ÖúÙªí€äôû Áž"ÁZê‘`$Ø]$X«8~‚™QI‚‘`C·‘`©‡ˆäö‘zHîa'©ù^çÖU[¥‡¥¤¶9÷Yo–ý>Î-ŒßR»f¾háåR!ðÚáµµqFŠ©m¬- -šÿþ¾§Cޝ VÛ¶[†øÎÊJŠd€Õ ám¹ÞG\s#ú«ÜuR3”rÄ1ªi­¢rÔúI0‚!™`Ù§ÚF_çÄQiþÈÓ€{³ÁJ‚-²?»ôû¸»»;½ûºâ±Zu›v’`QVmÓgmûÊÇ`õm¼sŸJ‚ ‘`E9Ô’9Ö*ÖZ³®rû4Jü¹îý>H0¬O|Ü%0Öþï}ýœq 8—$†Î%fJ¦’œ#Áý>H0€\ë$Á F‚‘`€~$ÆÚ?àZ'Á†ž'Áj‡7¦†5ö ¥ŒÔþJ}'*Åz¶ €~$H0í Á¶’`ge&œù]@0$H8BÛÑú}` ¸ÖI°å$˜À  ÍÈC¼‘‡€~$Æ;>ÀçI°£@â}¨LjèLé{¥¦ÐDžVV3ü¦¦.MmÝÈö‚!èè÷A‚·“`ßóó3ÿÜfqþ°ŸŸ×Ñt V’LTcµR£Š"—æéÉ,Kí³bÍ€`ˆ ß ®“`+H·¯m8k[^ã—ŽSÍv½¾¯o05,*½fH°ñÕ*¤¢C7I0@0D‚€~×J°Üò¯=gJ°ˆ4ºƒd$Á`“%؈á$ "Áè÷õû$ؘ̦÷÷Žæ{P‘e}'5_‹¬É}·å½ÜqImgí²Z¶/w Ró­_ß`Zaü–¢ñ‘ù"…ñSC3k ã—†xÎ,Œ]& $h'ôûxºKÉ–ÈëÖÏRòk¤+½ŽH°šì¬ˆ@Líí1¯Ù¶Öõ`¦K°™7D)!@0D‚€~·þûèõ׿#ï‘`qQU“A•Ë*-{JÛ\³½ë­ýw¯xlJ‚xŒ ÒA_÷Gˆ•–@¿¿ƒØª_9!V;oÍö<-¬EîÔ])ÁZ¶£µîV;“`H0 €`ˆÛp]ôû£úá\–VêóR&Xj¹ÏZ¶[W‚Õ,·§ð< €#Á†¶—`¯õ^çê*æê%–†¤×Ô?ŒÖg ß_U~õ|Þ’ý5s›î^¿G|ÍÍ‹ìKküÞúh¹å· ¼Ü¶+Œ€ "ÁeUô!!­¯k³¼"Û@¿ÿ„L°Ôw£l—L°ÚõÕN12+j$­Ûߛ͵+m—{J$ÁÐ2ÁF¿nÍØš!Öè÷w­ ö.ÂJÙZG2k·š`52®eèÜH©´«íóªçÂ=% €`ˆ;MR‘`ôûˆÊ°'Ê,`6’`07,$ 7ü #Á°ªÛùéªd‚Ø8"ÁæI°žÂø‘ø­Û£í$˜~ÿÙYa$®’`»>Q €{LðÐS« H0¬&ÃH0œ-Áþ{õÿ‘` F‚ m” ú}ìüÇâgH°wùE‚$˜ €`ˆ€3€ÿI4Ö|÷™¸Ál v$¿H0€›„­2”§e½ÑÚ:#¶áuÙw,$=³PöŽÑS F‚ðÇÈFJ°Tö `Ó‚°Tæ+…Wï~Ì”Ñâ×$ våõ "Á@¿•k‚^î².µ¢°þ8Fä `UAXîIb‘'ŽÕ<ìH´®?’ÑUÚŽèÓÒŽ^íOt{k·³öø½®É|k9ÿ5ç5wÌ~U>}.rìrm/rÌw:>+Á €ƒ§Cž)­jÖ}eÖ V¼jþK ÝvÀïËI¬”MRÊ^‰È‚H_ûºvÛZ2rJ£6¬eKb#7ifKÛˆóÖ{þZÏQí1oÍP»úø`$è÷1ó7,÷YDþ¼¿NI©£Ï¢ï¥dWj;J¢ª´œÜç¥ý„L0@Û½ ,Wï«E‚E3·JY25™A-ûÑ*ÁZ†Cöìo$c¨FòD3àF­³¶mD2õFµ•Z´úñ!ÁH0Ðïã¬ì¯¬æuThõ,7"ƾþ=j_ &ô±ŽÃÉ5ÁZ3„z2Tz3”Fg])ÁFg–<³·%·Î–ÚjgK°•ŽÏS%Ø{M’Òû•iÇ$ w’_gK°T6XjžQBj„ø"ÁÆ<2%È`$ 6@‚õÔª"Áú%OËÐÍ'I°;‡ŒüŹæu˼3~œF?4£4üw…Q´Á1· ãŸ/ÁFþ‘ ÁH°S%F€%‘ –{l{ôÉVµëo.2ë颵ä¨y2mϰۈ;{›`˜S?,%©j…PÛ· ÁH0…™<¢&Ø»+ekµÈ¬QYagK°Þ§õŽN-Oø½MH0œ— F‚a¶H0Œ ZðòWd‚­ œFe±‘`€~÷` `$ÁÐÈz$XíC H0ú}` ÁŒ, ]’ÖR¿T0¾µ}K¦YKaüÒ>ÍÜ&ú}` ÁŒsÓ@0t Kö@ Îôû Á $H0‚¡[=5k×óºÚöËôûXÿé@ú` Áì!KŽžÂXºY~Â|Ú  7 â¾ÿæç§´ølgò¿­ÿÍÂþ¼žÓ϶å  €`H&è÷±ÊïÜ·?"ìǼ “¸¯çô›k $!Áf>,ú„6‚¡Ñ5Á´ú}`Üïìë‚Iç ÁH0¬A‚ TÁÐÙO‡ý>@˜8§Î)H0L”`GÂçëß©ÿG¿•L©aB9 –š?%°ŽÖYÚvÁ* ýC¦_ ßæÈÒÄ9H0,„•$SD •ÄXíPÃèzJË+ÍÓ³l‚¡šì/ €~8O˜&Î)@‚‘`I v”=•ËÈê•F9iU#ÛrÛVÚŸ–ì3‚¡ùE‚ÐïçÈÂÄ9H0¬˜ •^3$XøjÍèJ  ÍüÑÚµ_©Ýî–§aêsý>0B–&Î)@‚‘`$˜á€`h™›¼{K°èo ý>@˜À9…ö¯]_R¿f8d©0~4C •5­YVæ­kÖRÛ €`h†ë}èG$Ó5ºœHFWé ‘ßš\Ÿß²Môû KHç ÁH°S3Þe”` !ßÿŽÌ~-=ÕwÔCHZ³~#]©ÙVú}&„‰s `$˜ÂÌC7–`#*R»œV6b¨= è÷ñìßÐV?ç ÁH0 Ý$ìÌå´Š0 Ðï‚H8§€¶N‚ ‘`$ è÷A$œShë Á†Ư-8_#Rëh-ŒŸÛÞÞ'«5 è÷A¤s hë$ @0´ií‹Ú"óp\ý> ˆtNm#Á†¶Í#{âOv<ý> ˆtNm#Á†n$Á@¿"S@[ @0D‚€~~_‘Î) ­“`‚0‚! ôûDÂ9´u¬û1÷#ºÜ2Œ€`ˆý> ˆtNm`‚F@0D‚€~D:§€¶Ž® ì]2¥ž¼•“`©ùSëhGë-Û†H0ÐïC çÐÖ(ÁJÁX$û+"ªJóô,€`ˆý>ü¾:Î) ­“`Mì(‹«ET•–Óš}@0ô$ 6jXx.CW]Fú}"áœÚúã$XøjÍèJm †ž.ÁFõ}‘L]ÐïC çÐÖI°“$˜á€`ˆ+gÓ–²¹Jýl$[7W›±&£€~DÂ9´õe ã¿3¥"õ‘ø© ®TßÓ!ÁL°q²9V ùMpú}@ çÐÖ—”` z†+-¯f}$ý>‘pNm  -+Á"4!Áè÷!ˆ„s hë$†H0 €~‚H8§€¶N‚ †fK°ÖÂø¹ú=La|@¿¯ß‡ Î) ­“`CøGÿ%*¸V;ÿ®@¿"áœÚ: @0äGîv’I ß×ïC çÐÖI0‚!èè÷A$œS@['Á†H0Ðï‚H8§ÐÖA‚ ‘` ß‘Î) ­ƒ ºka|؇ý>‘pNm»ò‰cï߉nžylF-{µå¬²O:¾+oÌ!<ï$‘pNm[H‚E¿1ô¤}ÝY‚<þWï#ÙL‚‘`H0@ çÐÖ—’`©ÇÚ·©,ª£e´|?÷^$£«´¥,°Ü±HmKéuj{"û=Ö#çÙm_£óæ¶=×n®f‚µÈžÔÿß÷¿gÛZöåÉûÚ³ìÏÓ Ç›#Á`ú}"áœÚú¶lT@þž9RÊ`‰fE–¥u¼v`«ìëÙl•ó´Âñ&ÁæK°3¿×ROqÅZyH0"áœÚ: v‰« ŠZ³¿Zj‚­$†z³Úž¼¯gJ°•ÎÓ Ç›;7lv­¼–zг%y`€ Ò9´u`'K°ÚLµ;É•Õ÷µÔF ó=û<­¼Í$Xû\¯Œ®­‹xF=ÅhÈHMÁHÖ" D:§€¶N‚ -Œ TRóE‡Ý¤¤@ë0¯hÑðh`ÙRo)²/‘¢èµrèŽûy˜ÂÈ"óg§U·9U N0T'Á¢õöFÿ¡bF=Åh[œQS "S@['Áª2Áž6TåÁԻЈdÄÝ}_köù¬ã2ò<­¶Í‚¡¹¬El·ÊÿQge ÁA¤s hë$Ø©¬%@&Àìë*û¼Ò±Ùq›CkI°QCÍ[ÄÓC¨è÷A¤s hë$Øi…™@0ô?ÃþÑ*H0ú}@éœÚ: F‚ ÝR‚ÕÖú›UO±TÃqDÁÃ÷è÷!ˆ„s hë$†N’`†XÒ è÷A$œS@['Á†H° °]ëMú}ý>‘pNm ‚þ€~DÂ9´u €`ˆý> ˆ„s m$Á ú}@éœÚºc& "Á@¿A$œS@['Á*‚°š   ß‘pNmýLà†@‚Ðï‚H8§€¶~™{TïZJ`½ŸzOF  @¿¹¿/xßó´u,„¥äVÍëÞï ‘`†£Ðï£ÿ÷åß¿O žÁ öóS„€¶N‚M–`¹Œ±\f™`  ù‘3€~$FH°[_wÄH0,%Á¢‚‹C0€~$ÆJ°»_wÄH0`C7‘`†£Ðïƒ F Úú…ñKÙ Ñù†H0ÃÑè÷±Ž;*À>¸ŽÕá2[×Óó½ÑûG‚€¶¾ÁÐ^L&.ý>fg‚XÞû2^ÿ=RÚúNtY+ˆ3 ÐÖI0 €`ˆ#Áè÷I°‰ré={*%ºŽ2­j¤ØÑw2·Jó”–[únô½Úõ‘`Ä ­“`C ãŽ@¿E%XIüäUkYwjþœ¤«Éðj‘y33ÆH0@['ÁH0‚¡­$è÷±›‹ddõJ°Ñ”ËÔª•t£2ÚH0bÐÖI0‚! ôûØ4¬eþÚañUóºfµâ­õ¸‘`Ä ­“`C$è÷±‰kÉ´ªÍ$ëy-†ß+ÁF?ù’#@‚ "Á@¿‹ ã·Š¯ˆ(J ·Œ ÃL ÙlÙŽèòR"pVQ| ÐÖI0AÁ ú}ì¦ôîßÝŽ hë$˜ €`ˆý> F‚ÝZ€‘`€¶N‚ †H0Ðï“`x$ ­“` a_ã߯¼»òû=ë C0D‚€~$H0b$6`¯Ë>+ؽž+%ž‚! ôû Á@‚ Á0X‚½ÿ?'‚j_§>O ¦£÷¶óh»rÛ]vÄ+ígj¿Kó_A†Ö“`‘ùGõ©~ëŠ?nÌøÃFͶ¯¶Ÿ#ÏñUÇ¢e9©ßü•þ¸tv[9û^!r/¤ß'Á@‚€¶¾+9)a ŽzDZîF¯$½j–]#õ¢7‰5ËÞ=ÀH°µ†£÷~7·ŒÝú¨» =±½W‹^yµóïã,Q}åq!ÁH0`Ä ­o› –ËØ:K‚E2ºj2¿F¿n I0`• VÊ`íÉ(-eÜŽÊ´-Íô:šÉÒ-íc®.kä¾åŽÙŽÇ¢”j‡5Çaö¹¬½FGfz×Þ;œ•¹®ß_ç÷Oヴul”+ žY™`57j$@‚=M‚Ì`][ÚÎè~ÞǨ˜‘9ܳo3§®:=ûVÓ.ÏÊ?{ñª™ëú}™`¸&Œ´ul¢‹Mè ¤H0 $ öïÓûÖ>µ5s¬U¬õfº,g‰Ç"ž‘4ëž`Æ5šË@«ÉF[é~E¿O‚#m}k Ö2ì°gXGt}½AWMÀ×Z§¶è¿Âø ÁH°%Ø™âgæÓ‹w“`O;µ÷ ;I°³3ÁH0 j‚€¶þX ¶kaX$ F‚‘`÷:µx»‹“  $1ý·ã@‚ ÁH°A3½O‡5¼±ôà•–uµ~)þz†wŽ(Œ¿Û±(­¿¶]9º0þ̧[F²æÏÎ\×ï“` Áˆ@['Á`:è®§w –g¯j;ÞéX¸&ôû$H0bÐÖI0 ] †µÛzMVŽc±×öC¿O‚#mÁ ú} $1hë$Á`Hÿ @¿ $1hë$Á ú}` Áˆ@['Á†Æ€½øÐï“` Áˆ@['Á`ø[ÿûý7??ÆÏã»óG€ýüìßôó$î)ÁˆþGòyÏúMûÁÕí‡@‚í¤|ûó£ðnÄÔ›]`¸^‚9îÏj_#ÿ¸¡ýh?ÛI°Èü£ÄÚ×rr+o]WÏ÷V}túÌms査™×  LûB›Ä7Ì]ûÑ~nŸ Ö³üW 6K°)*Î<Ö£×õôsp•Œ"Á€ $îÒ¾´ígëL°#IÍàê‘.¥ÏÞ??Ú†Ò<¥å–¾}¯v}Q9©õ”Ž×SÏAÍùˆìkiþ;d»‘`` Á }i?ÚÏm$X)[kDæMTÀ”dDÐȉ½šlQû1j˜à¨ãõÄsPs^"Û^ûš‚LÜU‚Xîk<4jù=ßKm 6´ÞÔÔs©M‘`§ñ=™`­R'•ùS³ŽÚ̦šeö³Ú $ç ïœŒ_$ðô5”ù ÁÞ–1j;[ÅIϲV’«J°çúhYÚ ¶¼)¢™G=ÒgTæQÏùhò÷Äs@‚‘`Èà O_)ÁrY8ïŸÕŒ£ï­«4Oi¹¥ïFß«]ß%X)+«õØiSãÛ 6x=³LKý­–ezêâYì©ç€#Á Á@‚'ÁjÅFTX´®ëhþÔ:k‡ÏÚlÜkmŠ.1j$BTRÌÝŽ–a…¥ýˆJ¡Hñøˆø‰·Ê9h9¥aš ã j‚/Áz¤@.«¦f#³H°¶¶R{쵩I°(#â3Ö3BÀÝ%gâ<÷Ü“`` ÁV“`3²‡jÖ1k?H°¶¶2B‚iS7Í{’€!_œ``óŠ;÷Öž™ýd8l_ ͘™¹üÑÂbôS ïZ?u¼z%˜6E‚ ÀF¬5°$®=sVM$ì –+êÝZ<7®´¼ÒеÈP»¨Ž_‹›'H°èq®†Øz.µ) H0.–`¯M48úNíÓĢ˭‘eïA[IøÕÖ*m7 ¶ÞpÛ™Ew”¥w”`g>%S›"Á€`a Öû$±3k-Õ|¿eHSËТ³ŸÂG‚ɺB8ܹß.lÕc÷ô6E‚ ÁÀ©™`£Ÿ$ýwdxO‹k‘zQ1Í|#Á<Àƒ´`@‚a« ÀT¾ÿæ›þæúš`£Ÿ$ÖòIJ+$XoQkÃ!I ` Á€Ãm‚D×)0¯ÿÿÍÏO¦¿!Áz$Xm†Z«L#ÁH ` Á€  Q‚}èÿ¨ Ö:0ú½–§÷ÕŠ¶V‰}â[é‰nžIb$H0 Á@‚Ðÿo ö„"Òw/NN‚ƒöC‚½§Üÿ:Ú—Òþ±ÿ©uÀ H0@ÿ’‚ÛM‚ái| •`Ð~H°’©vRòììíA ¶â“Výí{ÿí¹ýÚ†þŸ ¦}áuxð‡L0,Ñ~.‘`©ͯÝ”FÄSϼ©mI½NmkôF¸´¾Üçg€  Áfþ1i 6rýÚ†þŸ ¦} VL%ù‘J5ó¶H°šuEoŽKRmÄ:A@‚%¦jþøýÃStÙ‘2¥?†å¶»æk5œó»®ÿ'Á@‚AûÒ~pÚ`Q¡S+¶Jfµ7ë=ÛXszÄ—›e‚lu ɨ.eƒ÷ü&Îþ[ItEׯ=éÿI0`о´`ÍÙ]5"ŒA ¶žëùM,e|•2¼R`«ýc[Ë2¡ÿ'Á@‚AûÒ~@‚"ÁjD ‚@‚a= 6â÷4Zë³·Lƒßsý?  Ú—öƒ›K°ša‹µÅî#Eækk‡´lc䯶¶VˆÂø$Øn…ñ[‡’`Ú”þŸ íKûÑ~–”`QÜè»çµšûãLô<­ò¬¦^hKaüQ…ÿkׯÿ×ÿ“`ÿåþݺŒž}¼[N‚ vÒpH ÿ'ÁÎ’`#öm÷ ü蘒` Á@‚ ‚¿€þÿÑì(kêëß©ÿG¿—ËÊzÍš¬Éè*­7—–ZWn?së©ÙÆè¼¹}H}–;&$H0`‚‚ Àï ÿ¼ËI¡Ò¿£ßñº÷{=Û’Mï¯{÷§EÚÕ¼&Á@‚~ÿýÿ£%X4#«VÎD—_»ÞšåµH¥H&XD¨•²³JïÕŠÇÒü$H0`‚‚ Àï ÿ— VY§ë ™T#¸V“`µõÆFgÌ‘` Á@‚‘`A€ß?@ÿ¬:ó#1@‚AH0úÿ›Ư•B5ÃýrÅÜKEî{† æþýNë0ÎR]µè>FŠæ¿¿ïé Á@‚‘`A@õ|èÿ`O°®üþ ûD‚ & ü¸ð úŒ«Û–Tæ  ÚÏX öý7??oT>7Àu|u÷ÙŸÿ €~~ö3‚`  {™’(üQ{¤ƒösµûöG„ýp9¿n¸OŸýË7A H0Àpx`¸‡sÜŸÕ¾FK0ÇUû¹T‚Xë¦ÕqH0€ƒ $H0h?$@‚ Á ‚L`P ú' Á¬ ÁH6`$  Ú—ö£ý`ÈH0 Á†>½óý;­OÜlÙ†»^w»J0í‡,õÿÔëwyVz?7ÏŸ¢÷—Ì1` Á°½km{Gßë•#®ƒ3®¥Õ¯×3%˜ö£ý`œ ÁJâë}¾èûµó' Á°ŠXí–•A‚ÅÚ×ë9yý£ÎûçGçòýóœ€H-#²ÜÙñúýR›Émsj¿JÛý4 ¦ýh?$e‚E$ØY¯ ;H°ÙC}vÌʸb¯’`µÙ7)ù‘’Ñ,ŸÈëšïG–Q³ï½ÛzW ¦ýh?$…ñ Á.øÍ}ÿýý dJï­.Á"Y©ì„Òü=Y ;fe”–™ „Kóõ³Wg‚E‚ûiýwi¹­£EÊDÅF4séI™`ÚöC‚$˜c, Áj†I’`À¬ô;š_9!V;ï ¿ë ÖšYÐ’}±{VFi™Ñí/—%؈6Ñ"ZÚçh‰ÑòÝš6ø”š`ÚöC‚$˜c\(ÁjŠä] ãƒë¿–sYZ©ÏK™`©eä>kÙ¾]$XMÝ–¬+³2ZÅ܈}!Á®•µmµµÚöC‚ Á€%$ØJÒ€< V/¿z>oÉþš¹M«J°3%ÓY£%ØÌÌ‘Õj‚µáŠ~¯´žq¢Ú’ØRØ<2L÷î5Á´í‡å$ð & $Øõ™`©ïF%Ø™`µß=ªWõ Öš•‘Û¾ÞZDOÈÛé)šw{°Â2Á´í‡H0Çx`&@‚[ì]„•²µŽdÖ™5ÁjDZ®{KaüÚ¡Ž‘"÷+eeÔ³ï-äY F``ÚöC‚@‚$Øãû®ˆ|»{@&(%Á@‚AÿD‚@‚$ØÃ³Çï.Á0 $ôO$$@‚É d 2I0` Á `÷—a‚LA&  $€­†6ä 咽r¥°>@‚$&Èd’` Á@‚Xþé Vó÷3ŸZ`^%Ø9ic¾qó 2™$H0`.ÿ«léóšL°È2j×ï|á‰ìõ8 ä[@¯8XYjœ¹lç@&d‚ í‡6„yJXõfÞ&à ¬WœŒ0©ïD—5[ÚÌ8N3—}Çs@‚AM0`ù'}¶lÛÑ2zöÑÓGµí‡ܘvd‚å†@D“. ˆI°’yÏJe •æ)-·ôÝè{µë‹Š È1H­§t¼œ ž ì*‰1bßv¿þŽŽ) ¦ý¬Ò~H0àÆ5ÁÞEXä†77¤RM0 O‚•†êE²™Jë¬X’,#3­"ÃG/ç€Cß=Ŭ êríŒ}H­cæy¹J‚e½|ý;õÿè÷rY5GC#9¥õæ²xRëÊígn=5Û7·©ÏrÇd¶Ó~´ €Å3ÁZ¥N*K¨fµ™M5Ëì²ÉÆ:*–>bÛŸtH0DK\)«Z×ý^ï¾]Y3ín,Ô—þýވ׽ßëÙîTÛ{Ý»?-Ò¥æõ ¦ýh?$‹K°¨t©y]³ŽÙT­¦vÿZ²³œ }ò«U‚åþŸ˰Hs‘õt¥e•²7"Y-ÙZ¥,‡ÒªÙ,¸R‚E3jjƒëèòk×[Û†Z·»G*”®ÇÈ{µâ¨4ÿ, ¦ýh?$ ÆŸ)`Zêoµ,{ÔSÏ’`Î †þò-¬%³¢”‰P“Õ<ŽÊäh­4"»ä.™`5u–έâs‰ÑsíŒj“gg‚i?Ú À‰,")rEÞG ŒnG˰ÂÒ~D¥P¤x|DüD‹Û?õ`8»ðtKpÖ+z¢Y 5Y"­ÁsÍöÎI°½$Fm扡ýh?$…êe$Xo å÷á`ØU‚ÊðQLJë/l^Ô× ×Êã.)ïêÕ’8¢]ä†×@=ÿ̧Cj?Ú xšÓ/‚ x†#_®—`Î F‚ÝG‚å:ìÚöõ䧉®x¬ZÚþ, ¦ýh?$ðpVû>€k%ì.,Zà½gR$¡¶u©HíÓ!k?{ra|cÑÓI0íg¥öC‚$ Ápø$ɳ3”rO+ †çµ/íGû!Á"¬ú=$@‚aÆpH2I ` Á` €»¥ƒ Sû  ÀåOƒ$À Á È  Ú <²> Á È  ú' Á` F‚ ¾™ÚH0`va°¯Ë-·u¯OV¹= F‚A©} à öþ{K°ëè] Œƒ sµö5º¶,ïý;_lZeŸH0íGû!Á€Ûß°~áx×J°¯Ï^³¯Þ3±J¯²·Rë¬}?º~ $ lW V³ž^‰±¢˜-wZ·oW ¦ýܳý` `’K‰°¨„:PµBªU‚ÎFH°çI°ÙÙÈòŒà1µŽ׫$Xê¾ÞÓmëûgÑ÷RÙ6ÑyRßËmoi?þŸZFi9¹e›Ô¾”Žë*LûÑ~H0<^üX†ï¿ù&lìë3%˜á ÁÜ_í$ÁF,ë*E‚ýê~•£­ËíÙžˆ¬i¥5ËŒné¼ì$Á´í‡ƒš<N \óóS„ÍzøÃу "ï‘`$H0Ì“`¥€,—É*hÍŽ¨Éâh "k2%"Ù*$X½4ˆd¼ÿÑ.¨G¾×ÓVGKŒ‘ûò ¦ý<§ý` ÁL`¡ª’¤J‰¯œ«·ç©ª£$X´VX«*Õ + ÔÏ‚#ÁZ²™z30JÛГ‘ÐˆF%ØÌu’`ý¢£·ý¶ É]MbÌØÆ%˜öó¬öC‚pjàZª]—ú¼” –&=bý3$Xªø|NBµÀOÙoY?@‚‘`­AV©ÆN­j h{²1rÛ0cXÖ“$XKVÉŽ£6ƒ(²}-YIw“`ÚöC‚°\àZK‘Ï[²¿"5{?G;H0¬/œ%ÁFØ'ÁúÛW´ v´`yͶˆˆÊÚ¨Õ$´/í$ ]Z‚åÄVêÿïr«V µ!‡Àµ¥Ñÿú™`dj_ Á@‚$ Ö-ÁZ_§2¾H0ëYYaÚ /!Á ÈÔ¾@‚H0lºëɃÀu„ Ó¾} A¦ö $@‚=¬0~MæVí÷H0ëʵrœs@_B‚A9¯æîÂÇP í‡ †S’\àŸ*^ß+ÁRË~ߦ£ZcÚǵí$÷´Å;̧}$ ¬©}}ÿÍÏÏÀös½¸7ÆÏÏó®ýàêöC‚ p” ¶²ˆÔ¡¶!¾ô©Ð— Á< pȯQltM. ú $k×ÐG З X"kÀ3³¾ª„H¯Ô°ÇÒpÈ–¢ù ムÁ  ÀàÀõIìí Á ` ¦ÿ@‚ „Wý `€ ¸ê}  €ÀUÿèKH0B/ª‘tNÁìQëˆ>a Áœs@_H0@ú¡4{{gI°3÷×ú3ù Á I°£Œ«œ¼9Êlª?" rë8ZnÍëÚe—2ÓJǰfgc`$<àI³$-ÁZ%Rïë‘YUïû’úÿ»,êÙæÖï®vŒA‚‘` @‚K¼®ÉKÍ¥ë‘JµûÞ*ÁÎ:Æ ÁH0@_@‚œ5«¾ÖYlıZíCàJ‚ú€n ¦(ë¶|ÿÍ7ì¯bÝ­#A¿·0~MæÖ•Ã#Ÿ]yŒ!pÝ]‚iÿ ‘`nÆv¬£ñ×ÏOöôÂø¥"ò#æHŸÒ÷j¤WoaúÒPÅš¢ùgc\ï,Á´{€$“­ BµÙÓD«c¬aèrM¦% /!Á 7Ž$ ÿYF‚Õ<üAm<@_H0¸qT˜Ðÿl]“pƃ(}‰t°;³¾Ï¨uäj!¹q$ÁèH0@_H°ª§âí"Áv NH0úŸë%XÍ0I ÁÀ$XäÉu5¯k—Ýòä»R½©Ú§òµ<ÅÏ# \÷’`5Eò=lЗ€H°œØ*=i«&¸HmëwG s™™@‚¸ê} À $XT*e|’`µëY?M @àªÿô%–`#²²fgŸ¹q„Wý /!ÁÀ ã×dn]91òY¤–XËünë‚Б™t«;l'@‚З€GK°œ˜(ï•g¥ø©!‰5û‘+ÔŸÛ¿š¢ýnÛ S·Š£ž*P‚À•ô% H°»Þ”­~Lž„æ„cDt¦2órB4"6£ï•²Ùf S@ÿC‚ú@‚¹Éwã¸q&XëpÛÖ'Ö\'‰vÖ“Dý èK æ&ßãæ¬ô„Ζ‡D–Y#ÂF?¸Ðÿ`€¾€sãøÐL°ššs½Oý$Á ÿ!ÁèK±r6xßóƒGŒƒþçAìÌs´<4dÔƒYz‡ˆ¯þàÑçè‰R‰üž’`pãû'·ÌáÏ}áÏOF‚A:ùé¥'t– ãG‹Ò箑ÚÏƇþG&Øqq…„ù‡ìZ1uÕ¶‘`@‚áŽìcéßsL* ÿ™œ VÊbí‘Ý%©ßòÔÜ–'ß–þpˈ­ùÃFô­Ù¶µH)mkÍ1ϳž «H;¨YÇŒ§“`@‚álõßscvoÇ  ¶°«=J‚•ÖÙ» QAÒ³5¢§$€JB®uèzëv•Ò¨6QÛ®xê1 $H0™`¨¨ÉB‚ ÁVÏ;[‚E‡}¨yØòï³dST‚õÇš ´h6ÖŒìÀ^ 6ã’`@‚½Õ•úÕ+Ð^—YÖˆõÎ~¯ËL-ÙH‚#ÁŒ»µ«•+w”`3öi„ "Á»I°Q2§U¾õnÇì}#„! &°„’`ô?$ØM$Øìí"ÁH0@¿+Í›@)Q”“Z¥yJËKmSjûj¶¿´ Ös¬zÄ A( èH°i"¢WHôÆÏƒï‘MÑâí-EçGmWîxÔ<Œ e]#aéÁg?õ˜,7oIP•2¹zeW4[,ºžší?zút]-ÛJ‚aÛ ôs~ VßÿD3Ì}þzvic½’úÀ}%XDÞŒø~)ë+’YÕ+¢¢,š W’$Øf7Ën8™`®úŸõk]þE€éK$Øå¬&K*5¼1RÐ~F&XnFe‘`ˆª»ÝH—Úä— ¸ê} à™…ñw“`-bjæv‘`A ÖRg#÷T¦Öú5 o]ÿ]oï"¿ÞÛçˆâÍWˆVA4®$ H°rüšbï5¬Tt¾4Ì1Zg«¶`ëÌÙÃ7#Áj Ýæ^—ŠõF¿ßZ;$²þ;Þ8Þ)ûkG f¸.®Ï®  … H°SY}ûv;$XàzeÕÌÇÐï„ÞU~•Úgéi]5Ob‹¼ɘŒ>E Á¶é¾ÿæçg þ¹ÏÆñG€ýü¼Î@‚Í>-YNw_£ ÖùHx¬¾€ðS†#µžçÖvªûĶoÉþŠ ilùœÔÀþì5"*ÁÞç¼>ú^iJâ*’ öºÜÒzH°›H°£eEßÛEŽ]µíQ 6*,õݨ“ ` Áj²¿J¯Kÿoy=b8äS‡KÞN‚ýÍ^f„LjÞè|¯ï§ÄÓÑëáö¾M¹Ï#ûuÕúgÕ{a¥l­#™µRM°Q-wþ‰:`@Y‚eHÍ`5ëU¬ôol ÖúzÔ|)Ö*AŽ–Ñ»ŸW¬ÿ)AèìLÅQŒ ì¯áÙ_=™`£jE3½®¨oF‚m*Á¢™d-2c´øZaýO’`ÑcÉ&,»šìÇÞv`Ðw,Ä÷ß|Ó~q÷ö{µëÉÀ"ÁH°%3ÁZ¾O‚=C‚¥þ?»Ý` `½ûƒ›L‹LîW~Ší×t÷ö;r8d$‹«TH?'£Rßm-Œ_Úî'Ö{œ+ÕÊ"ÁH°3%XM6am6 `H0“)->ªêõ:j¦]Ûok¦£ÛhlëÂø%Ñ:_ïpȨÐë)L_»ŽÑë'ÁÆHÅÙ_$H0 ÁLÏ”µ­rÔL»¶ßQO‡ v©;4µ‚zAèB…ñ£²•ô?H0“‰3™H0`]Ã!!]Y‚¥°z[³O‡„þ‡@‚M¬Íók¥ý{¿/êY‰ðL 6k_—«’` Á†H0BW‘`3Žaꩤô?H°«eÁì}Œ.ÿ}¾Öí"ÁH°3¯í” F‚ BI0ú$ئì(Ãåë߯Ÿ¥æKeÍDב˾Ie‘`$XîÜGÛO®½j§$H0LJ‚Ðÿ Án$Ár¯S"¬f9-óµl+‰@‚õ¶í” 6­0>Vçƒ@‚ ÁnŒæ2´Ž2dF¼.­côºI0¬¶DÚ vJ‚‹J°ï¿ùù)U>7ëóG€ýü½÷.WŽÞ‹,+%™ŽæÏÉÜëÜöEAjþÜ:Zö%"írDztNH0€H0 ÁL$‚ök"Á%SDÚ”dNnY¹íKý?²}5"¬´Ÿ-Ǩ%s­ç¼`À¬eYïßùOÇ6qFíóñu©×;íãC‚ ÁL&̤ýî%ÁZ‡+Ž`µëjR¹,±š}$Á€›H°£ïõJ°3%î’ˆ[ëøÌ8f$Ìd"ÁL$ØŽ¬$§v’`½õ¼H0` ö7ÛÉÎ:š/’Ñ•z/·î¨ÜxÿjÑíz¿´Ï¹õG²Û"ǸtrûٮܺjÎcêxD¾·ãñ-ÂH0$˜ÉD‚™H°;J°HöØ, Öº¿$ †›J°HMT˜”2fZ²uJr#"nz·¥´Ï5Dz&›hĶ÷œ£ÖóÝÖšý_ñø`H0“‰3™v+Œ_[ƒ«¦VØ( Ö+ªZ ÑG>‹f¼ÕË_ ãëH°hæW¤ÖWTŽ´ ˆVySÊ*ªÑåG3®Fʈ,,mç¨öP+V?>$Ìd"ÁL¦3%صYQ³©ÝÆ3ÖS[ÃlH0àÂL°A4*ûæ ÖZ³¬7»mdæÜÈL·èy- w=>WK°ÏùõefÜkñQõÛìÚ~{3ÁV‘`+ʤ <\‚õÔ±"ÁƇ|ª[e8ä×µ  Ìô„LšZ 標vm¿3$ö–w$pQaü¨è(oÍê* »–ao¹íUÛ¬÷AÑzc‘ã^SO-—ùÔ"#{†®z|®x:$ù€3‘Ú¯‰ F‚lVfK ¸òû+ïÛ·õp<ÿér?tä̤¦’ökRŒ"ÁH0`ÀpÈ•Äf[ŸÊÑì/$˜‰D ÁL$ F‚‘`À„L0kH0ò f2‘`&Œ#ÁH0€n+ÁÈ/$˜ÉD‚™H0 $@‚O‘`?"{>Þ|æ3Ÿùj纚H0“‰ F‚  €L0“‰3™H°‚ÀùOÝéŠï H0€ÔÌD"h¿&lyCr‘` ÀÓ!`& fÒ~‡K°¯ÏÞçIed½Îôúýû5Â+õÞû²Þ·9÷Yo– F‚$°{½^õ[H0‰ ýšH°ÜðÃh&VJ~‰¨–,®ˆKI±YÛD‚‘`p“[ ˜§f]Ñåý§“X“iäqÁzÇqT›éÝ¿×÷K?t²Â`&Aû5‘`ÁÍÂ*½îɺJeŸEþ]ûš#Á€Ë3Áz–…„ŠHŠ»£]ÅÝŒs]+\gîSô‡Ž @‚™Hí×D‚H3%SIΑ`$°E&XJ¼¾ßšis´žÔ:Së)­ûè³Ô{ïËzߎ¨Pé9&‘夶³öܼϗZvt}­QÑs=Cˆµ´—£m-Ít koôõeH0‰ ýšH0Œ#Á€É,'z2krË+­³w¢²£gG Ûl‡¥mHɯVy5«})ÁjÎAä¼ç^¯þC€3™H0“ißáµu¶JÃ!k ×G¥XÏ6‘`$0-ìl YOï6D2xf—÷§H¶Vê»-Yo-Ç9º¾ç½çøÕf¯ÕÔ®#ÁpVÿÊ<.k-H0 f2ÝC‚Þï>´¬0 F‚$Øäm˜)ÁFŠºÙ¬eäl 6¢¾ †UÄVøÊ ±Úy ·‰`"ÁL¦=3Áž$¾ž"ÿH0€»µëÍ«yÊæ, 6r8äÌL°QA Á0ò/º5Ÿ—2Ár5±~`& f2‘` ÁÜ8œÔ*FZ ã§¾×"Ý¢ÃGfƒåjx–w%á4¢0þÈ6U>ZÛ^jÛßŒÂøØ_~õ|Þ’ý5s›pO9 ¬ÁG•vm¿$H0`‘›ÜÑÒaÖzv`Åý|ʱߡm`2Áj2ÁRßJ0™`IcÚ!“¦V‚9j¦]Û/  X( c$  ½5ÁÞEX)[ëHf© ÁD‚™L$H0 $ f2© f2-W †Ó’` `@‚™Hí×t{ æ–ùiOÂ$Á Á` ™”˜øZΈåÍ%-Çî‹÷côz^ߟ-Á´_íw¥öK‚<ý ò 7Ê0ßI"ônÏÌý¸*«êL ¦ýj¿+µ_ j‚sor¿ÿæçŸ û?Aøç…†ûâ\¯|nþ÷Züùymê£ì˜ – >_ßoÍy_ÏÑrrË?µGó–ëÒ~E3FVÜš}jÝ–ÒúÏÊÓ~µß«Û/  ˜{“ûíûñ–ò·Æ¹^›Ïkò›> À®ì(H1Rä–ÞèëÑû‘*WïGÍ>E„EËë3%˜ö«ý^Ù~I0`ÀI7»Gÿ‡s ÁF÷gJ„h6H$gö¾í°­û4jWd‚i¿ÚïUí— œ(FRÿ†s Áv5ë, }ë]ÿYÇnö~`Ú¯öK‚#Áp[)BŽ8׿O’½ÃÓV‘³öƒÓ~µ_ $ bÎ5€k D{EB¤ðöÑ0ªÚ‚Ü=CØ¢Y-+ïG´ÖS´èø.…ñµ_í÷ªöK‚."äˆs xöoF”wNg­Ç´ïÔ"Á´_Ó®í— œ(E¢ïÁ¹È3™V•`Žš‰ F‚Ĉsí\H f2‘` ÁH0§f”sí\H f2‘` ÁH0#p®$˜ÉD‚™L$H0 »Hoý Î5€3™H0“‰ ÜBšÀ¹`& f2‘` ÁH0`& f2M•`x$@‚™Hí×ô8 öý7??ÅÈçrïÌ×±{2ØÏÏóN‚’`Ñí:cûSë `H0í×4Y‚}û#Â~<€_ÙÏŸçû `‡ó]µ$ ¦ýšfJ05ŒA‚:8×€“$Øëgï¯SÁûÑg_¯_ÿ_óý”,Hm_)Ð/-'÷yiûMëH0íWû%ÁÄ+ Ás @·D¨} òG½®Í€y—3ÖiZK‚i¿Ú/ &^  œk vJ€ŸË¸ÉeîŒ èGˆá9Lû5‘`â`ÐÑàäóô~ξn¦"ï9–€U$¨`D ´_í—›‚t4›û”øÊ ±Úyµ@T"Ôd¼¦Õ$˜ö«ý’`bS`€Žæ‚ã}”¥Uú¼” –ZÆû÷Z×x®ËÛ-…ÅSóE–]Sp<ú¼Üv),~§Cj¿Ú/ &6 àùÕóyKö×ÌmÜó4Ñà8< °MgI0í×D‚‘` ÁÜ,,õݨ“ hɤ©2LL«e‚i¿&Œ `£š`ï"¬”­•éG0J"˜L;K0“‰#Á@‚€3™H0“‰#Á@‚AGç@‚™L$˜ÉD‚‰WH0@Gç°uÍJ` >º ã;´_ˆWH0@GçpîoÄ÷ßüü Ü>3žÆWæV9ÿ+~~¶Ë öØö‹½Û/Ä+$ £s 8÷7âÛöã¡üzð¾¯Êg{üV!Á¾;fرýB¼B‚:8×ø„ó¸–A‚:8×ø„ó¸–A‚x9\Ë Á¼œ?®e`€À Î×2H0@àç€k™t4p®ð ç€k™t4p®ð ç€k™t4p®ð çp-ƒt4p®ðéü9€k$ £s €ßHç€k$ £9:؆ï¿ù¦ÝÜ9ŽàZ  ¡ãý׿þ…Åù#Á~~Š0í ðrþÀµ  vc öññûÿ?´[€ÀËùs×2H0$Øm%ØÇÇÿ#Á/8€k$Œ@àåüp-ƒt4H°×Âî‹Õ×úÛ6¥^“`÷Cpþ¸–I0@GC‚…äÍYr)ºüÒ|$Àýœ?®e`ÐÑ`Ã$ØQ†Ø×¿_?KÍ÷¾ìÈ|)ÉU»¹÷I0€û!8\Ë$ £!ÁНS"¬f95óµn§L0€û!8\Ë$ £!ÁBZÑÚ\­,µŽšõæö¸‚óÀµL‚:ì0“+'FK°hͯ֌/Ã!î‡àüp-“`H°j¹D‚‘`œ?Àµ  ÀÖ,7Ô°u¨cd}¹uÔ«¼¢(> xÁùö½^[qüH0›I0`Îðôk¶æ?×8 €#ÁH0‰ç Á@‚nH0 ÷C΀E˜ë›Ü4`$ ö¸þúþ›oú÷Cpþ $à¦#Á°Áõõïß73êø#Á~~Š0}‰û!8ÀSD˜k›Ü4` ÁH0à‘ìCŸç~Îð æº&Á7 $H0 x¨Ó繂ó²VRÕ³Ü÷r礔-]ïÑ6Dæ F¢ÀùS3x ßóÜ4` ÁH0,“}Õú½T&ZD‚å2Ùj F‚¹‚ó·x†ŽÎ˜ÎŸ{§ŸŸ"ŒÜ4\&ÁfбQË®]Îk€z†°½! F‚Í‘`5r*5¼±¶Ø? F‚¹‚óG‚øµMÝc#Ü4`[J°3ö ÖV ~' Y$ æ~Î  /Ávø½×1ÂMÃM%X*[êý½š×GËÊ-»”µUÊ芾¿Û> I°;J°^™)J_S¿¦þX¤À~íL`+Õ5rüœ?Lg `$H°ÛJ°é“úÿ»ø©•K‘ ®žL°÷Q@H‚$zž>ýÏ}’ól@§_½Çðu‘eZïëòœOŒ F‚U ¢Ö×¹l¨Õ$ØYû( $Á $Šóçü‘`« +–3b½Î9 F‚$Øe‚¨'«j ÖòZ@H‚$fŠÅù 6K"eiezet}/š)–Ê{ÿ~t¥í à†,ZkU Ö²­WH0(ΟóG‚í(ÁRr)%²R²)úºwÞšua$€Æ%ˆRË~ß–È6Fên•¾·ò> I0€ÃL‘B 8 Á®`IÕ2ß{†×, f8% `c †u’` †Y…@qþ@‚ÝA‚EÅ F‚‘`PŸ  Á*RÜ9 ÁfŠ0 $@‚‘` Ár}håCŸw‚Hq_äü-5REäg sÌ­sÔö´Ì ÁH0`2Á„3ÁH0Î †ž'R‚#ÁŒ#Á@‚$˜¾‰@qþ@‚Ý*Ó $ `†k P $Šó ÁH0` ¶W& F‚$˜?2|ÿÍ7¿Ï$@‚‘`  $@‚ ¾þM¦5¦?ìç§óûL‚$ `P —I°×ùÎg¯Ù¹íH0Ìt æi$@‚‘`€y’‰ÃBlä¼³ÄIŒ3í'ÁÜG` F‚~ä`Ñmx²°sózO v”õþ×ÏSÙ[©Œ®œ°:ZƬmH0“‰SÓÀ®ÿÒ1Ba|ì2 öTææõ~,%JZ9yÕ“ÍuÅ6$ØãkT½ôöå8š$˜L0`ßL0 Á.‘`¯réýuJ>}ö¾¼Úï§dWjûH0ÜY‚E…S‹„ªÍ›± öl)3Bf9Î$ `$@‚ •`µ¯S’jä:H0 †3Á`$ F‚Ý[‚‘_ `Q VÊÀê™$ €3Eä•Âø$ØJì(‹+õÞ»ôzýîÌ0€°±Ëeˆ `䌉Û]‚‰ªwÉu$¶RóÈ H0‹K0` ÁŒ3‘`$XY‚•^a (ŒO‚#Á`& ö F„$@‚‘` ÁH0$˜ÉD‚‘` F‚$ؼã]øpóJ‚$ f2‘` ÆÔ#À Á®;ßóóS°|v@wâ«S½Ç¾ü¯ûùy¾´[ ÁH0“‰;G‚ ÁtŒ Áîu<¾ýa?nȯ›íÏçyú¦ÝÞG‚}ý5øëõëû3åÂëzÏ\7@‚™L$ €H08×x˜kýl„\èù Á”€² $ H0 $Áj³±Þç-½Ÿ“Y©åŒ\7@‚É3­— F‚‘` F‚ Á€%2Á"BlİÅ fÈ$H0ÌD‚‘`H0# 8M‚eŽµÊ¨š,´ÑëH0“IM0 $H°¦¬«ZÕš ¦~H0ÌD‚‘`H0€s ` F‚™H0 #p®A‚µJ°ÜÆ–'L¶ƯY7@‚å%ØA6C²Õ.óïýþûA‚H0 Äœk`@<Â=@‚ÝE‚õn F‚=I‚‘k #p®ñÈL0Ï–`GT¹Ìª÷÷¾›š÷õóܲSÛyÿhý«í F‚-ÁH/€ˆ8× ÁÈkLUJ‘ùG ‡Üa?H0ä$ØW›{Ü×ëû¯×GJŠ¥–M #ÁbÎ5H0€»½;K5YOÿ~Ý&›z‡Cî´$R,%ºJ¯sÒ+òš F‚  Án-Áj¤NjX`ËÐÅÑl·ý ÁPÊ;ÊÞ*eyõH0Ã)A‚‘`@‚H0¬Q­.Á®Ü ¥š`1E‚$` ÁÜß$XE-­Ü0ÀÚ‚òQIÔS§ý ÁH0 ÁH0$@‚$Ø„ëÿ:ÆD‚íU,W¿¦Ð}ma| $ #p®A‚$Øö™`& ¶W& F°ô_Ú#8~¸ƒ«]Ç?³RÎY/@‚™L$ €H0Œ¸Q©øOÀÓ%Ø™E‚#ÁL$ €NXï"ã(»'õžcI‚9n¸Z‚å²±jÞO­#úþë¿G¬ ÁH0 F‚ Á€ÆÌ¬”øÊ ±ÚyI²çˆ0ç+I°Ñ¯{2Áf¯ ÁL&Œ@‚áñuœj>/e‚åjE•Þ¯Ù>`ÀÎì(  F‚™H0` F‚a‚üêù¼%ûkæ6aæ<‚‹ “$Á@‚‘`&ŒÓ$ †E2ÁRßJ0™`Ï“`Î%H0 $ f"ÁH0 ÁH0lSì]„•²µRÃ!Õ{žs^¡0~Y^)Œl ö÷þã¿ï½ž±îÈ{&Œ@‚À"ŒÃ Y¯ïÒ‹DH°VñTZöÙlÆwL$Ø$Ø×ïÿ×k=@‚ËdžÎ àyì(›ëõýhÖW$ìh=¹÷sâ*’ öºÜÔ~šH°§H0Ò Á §‰0ç«J0Ï•`‘aŒ%©5z™£†C.I‚ÝM‚½fq•Þ{—^¯8%ÅŽæH0 Á@‚H0¬²&Ø?‡cÚXZ. F‚í*ÁŽÄTJh¥†<¦dYi>€#Á€GJ°TM!àB¾ÿæ›þ„H°½2ÁFÔëŠfzU¨ßD‚­$Á"r‹H0€+lï¿þõ/` þH°ŸŸ"LB‚$ F‚‘`$ ` F‚áÆìÃM4 `'Æo©»>u©e½¿¯Ù~ F‚‘` €ÃÌM4 `íå È! vïÂø5La|`$ $ `·YÊÄ2™H°5$Ø™2ÀH0 ð#L‚EjTýz=N©× ÁV“`ÿ ‚Ï 5ï$ؼá& F‚É F‚ Áª¥LÏç Á®’`+‰' $ f"Ápm&@‚‘`€Âø$X“Ëe‚½g•Þ 6C‚ý}(T,3ìèý£ïç–•Z Œ3‘` ÁŒ F‚ÝH‚2I‚­œ Ö"¢RËx—a5¯Œ3‘`~¿I0€#Á@‚‘`›×{B—a“$ØjÃ!{DXø"Á@‚‘`& $@‚‘` ÁH°›f‚©F‚­Z¬VH‘` ÁH0“‰#ÁŒH0Œ#ÁH0 $ f"Áü~“` F‚#Áž[ÿh˜¤ãI‚]]¬õi’©¡• ãƒÛK‚½~g†D;Z&YG‚H0 ÁlÈõ{„{€‹Jªóžý F‚µÉ€3¿4?ùŒH0€›’ à¹ìïR<– ö:êû%qÉ«Y‰#ÁÖ’ ³¥I  $@‚UK°Ö×£³ºf¬ÇD‚ÝY‚}}ö:Ï—,>š÷ý³š×­ëˆˆ«H&ØërKëH0$H0ׯû€ë–`‘ª 6b=&ìî¬UjÕ¼îùþˆá†K‚#Á 0> ÌM4 `—H°…ó£™^³ ô›H°;d‚ebÍb-ëU¬ôo€H0 $Š…ñDø ÁH0 ¶…+Í7B‚Õ¬cdaüžu$@‚‘` Ád‚d‚Ý]‚ÕÆO§o)Œÿ¾îžõ˜H0¬]‚Õdu‘` ÁH0àQ A‚$Ø=2AI% v/ –+Ÿ{¿¶¾Wí:"íý»ÑBü~¨@‚$˜L0<&Œ#Á5ÁÆ ‡4™H°}$vO ¯sH0€#Á@‚‘`'ÁL&lßL0 Á`P  6J†½/‡d F‚™L$Øú € F‚ `DH0Ìd"ÁH0€#Á ¸X‚í6üø, v$¯þ^8ûWñ³Ôk€#ÁL&ŒH0 ¸!ÈfH¶Úe¾ø;ï öÏ‚²®ß¿²Â*%¯"ó“` ÁH0“‰#ÁŒSÿN¬w{H°5$ùÕ'Áj2¾H0`$˜ÉD‚‘` F‚’`GT¹Ìª÷÷¾›š÷õóܲSÛyÿhý«í vüCE~õI°Q¯Œ3™H0 ÁH0àÖ™`µ‚)" JB)2ÿ¨á;ìÇS%˜ì/ Áê%س>Ï“ 5ï›H0Lg `$H°m$Ø‘XªÉzz}¿E0¹Ó~Œó%ØŒï$@‚-/Áz‡W®"Á®Ü§I°wREòŸ>Ÿû€«X­\_ÿ~—a5¯M$Êìõ7>*ÁÞç¼>ú^iJâ*’ öºÜÒz À…ñKÃk ÊG%QOaüöƒ“ & ÁF‡ìa=â‹#ÁО V+¶^åRîÿ-¯G ‡4\$ ,ñ#|!5ÁH0€»WM°Z!E‚™H°k$ØQ†Ô V³žQ5ÁJÿH0—e‚žI‚$ F‚™H°s‡Cž 6ªöX4ÓëŠúf ÁH0$¶`»ÖÛ#Ál¥š`­O“L ­TßD‚­+Áz2°H0`$ðØÂøÀJLV `‘‡e˜L$؆CF²¸J…ôs2*õÝÖÂø¥íV $ Œ‚Œ #Á¬œ f2‘`{Jl4@‚$ ì6× `$˜‰á`$H0 p½¡‹naŒ3‘` ÁŒ F‚ H°×k÷ý:>~¹ÂÌÝ$Œ3‘` ÁŒ F‚$XézL‰¯œ«wfÿð%•^åÒûë”|:úì}yµßOÉ®Ôö$ØøšBÀu|`$@‚‘`ÀÞìëÆæëõëû3EHä=`¥À°æóR&X.ø±þY¬öuJR\‡{2`2ÁL÷Ë#ÁH0€#Á€­%Xëg3$ØŒïà^¬$–"Ÿ·dÍܦ+$XMÆX.óŒ F‚™H0` F‚[H°×@öH4½ï}ÞÒû9qÉ{ÏPË­2Áj2ÁRßJ°Ý3ÁFe Œ3©  ÁH0`ÛL°ˆ1d²$Á —$ÁfÔ{a¥l­#™uuM°šŒ- $ f2‘`$@‚‘`€Âø ,•5jhci¹$ öÔþ&UÔ¾§0~j¾È²k æ$ØžŒ€3‘`ëJ°[nî&SûCt’`$H°‡d‚¨×Íô:«P?H°$X)«g€#ÁþYÿ‰3‘`+H°ÕeËûöÕlïÎ"‰#ÁH0`$ l –ËH°½$Øß¯çãl­£×ï稜]®‰óû=N‚}]{Gï½¾Ÿz/²¬”¨9š?%wJ¯sÛ=©ùsëhÙ—ˆ´ËËÜ9 F‚»™ËÒoyÂäÑðÊÈr‰0ŒðD _GbkÔk æ÷{Œ«‘LiS’9¹eå¶/õÿÈöÕˆ°Ò~¶£–̵ÞóŒ ¶ÐöAÈ€#Áìž,š1v”!F‚™H°{H°ÖáŠ#$XíºZ…TkVÖñE‚‘`$H° 3ÁŒH°çJ°RQ|ÌD‚ÝO‚E Ûï Ázëy‘`$ ÜôG˜| F‚$Ø=%X.“‹3‘`$X­‹dÍ’`­ûK‚‘`$ $ `7.ŒŸ+‚ŸzÚcª0~¤`¾‰óû}~aüÚ\5µÂFI°^QÔSˆ¾4²¥h¾Âø$ ` F‚$Ø™`#Dc"Á®«É[›u†@è©­5c=µ5Ì@‚‘`H0`$@‚‘`ÅL2“‰»&l ¶¢Ðp\@‚ ã“` ÁH°eEƒû/`s%˜ÉD‚=C‚ ÁŒ F‚-.ˆ0`$˜‰ `$H0 Á†H°¿gúg&Öëû)1•ú~ë¼©× F‚™H0¿ß$@‚‘` Á–”`_홤æ}`$د¬|Šˆ©”èj™— F‚™H0` F‚ÛN‚­$žH0 m¬Gl•2Ìrï‘` ÁH0  ÁH0`[H°×à7švôþÑ÷sËJ­#Áp¾«-zo8$H0ÌD‚ù}&ÁŒn‘ Ö"¢RËx—a5¯A‚H0ÌD‚‘`H0S‡Cöˆ°ñE‚‘`h/Œß[ì¾TX¿f™ v ÝÂŽC¿{ýÎY­´žžíøºG8sßRë¬Ý¿Úm<šŸ%ÁH0`£š`µBŠ v^&lE F„‘`¸Ÿ›±ÜYûÙ{ÌGK°«Ž= F‚~„I0`$@‚m+Á^åÒûë”|:úì}yµßOÉ®Ôö™H0,Ÿ­ô>ON½ÏyýÿÛ»Ã]9qe£H‘Fùµßÿ‰æ­æfÏɾÓéÁv•mÀÀ´BÓØ˜6Ý^)[ïkÕ!"¬"ï‰Ô·Ö¥¶©I«Z™­ãFÛ%³½V~´ýA‚ã¬÷i’¥©•プ`H°ë%I5³ ºˆC<,+¶ÞePéïžõHý2",{ž­ýö(³·]²ÌtIÌ$ØÁõÝ‚ F‚$س$X&b¬yF‚‘`“`Ñh©Q –)§WÒÍ–`‘¨®ž2{Û|VN°Ö¿A‚$Ø‘` F‚$Ø3%ج(/ŒC~:äÑ‘`½9¹Î`½ù³2ÓKÏ`gå}#ÁH0€H0 Á.'Á2[$˜…»·‰4"ÁH0ŒH0€#ÁlÙ§C–¢Âzã—ö‹;“0ßB‚‘`±é‘(®hR÷LþÞÄø3%X49ý¨Ë&ÏïIŒßºžò‚‘` F‚sO!Á, ÁZX#ûX,$Øq9yW =u=ã¯Ô® ÁH0`¡/aà<>üˆ&Ál' V‹³XH°µž €  7#ÁŽ•`³E1l f±`$@‚‘`H0`$XRZeÊù7ÂÄo/`$˜… `$@‚r‚M”`¯Òé}½$¥¶^‹n+É®R=Œ³` ÁŒƒÄø$@‚í*Á2ëQ¡5r\€#Á,$˜ïo ÁH0`•`Ñ:v$ö—`¥h°Ò>$H0ÌbñýM‚$ ŒÛA‚a$ö“`Q–H0ÌB‚H0 $ØìU.½¯—äÓÖkïÇ˾¿$»Jõ F‚ý+œ¢‘Y$H0Ìb!ÁH0€#ÁlÂzIRÍ,ƒ0"ÁðûÓ!KQa™ÄøÑ©ŽÙ„ù v¬ÛKoìyìõû>ó_6 ÁH0`‘`™ˆ±Zä F‚!.ÁZ‘YH°¢ƒ³`Ï‘`¥z’\ ÁH0àÒ_Â{J°YQ^$  Á¶%Ø»œzU[òê÷hÎÿ ´Òval_ öõÚû>¥ˆ¬×ý·Ößߟ^¥mïÇz¯síµÑ(3€0U‚e"¶H0`kK0÷—`[Bª$¬Jòª$ԲDZ`¾¿çH°š8Ú’L%VÚ4¬%Øjõ›Q'€0å饨°žÄø¥ý"ÇÎ$Ì F‚ù`m©Ë®[,$Ø>‘`Ñh°èúHÔU)ú,òïì:@‚8L‚µ"°FöH0 ÁÎËF‹‘`l 6k}F~0 $ \>1~VpÕ"ÄŒH0ÌB‚#Á@‚‘` Á.%ÁŒH°{$ƯmoåótH ¶ÖtÈlž­ÖtÈlâú¨©@‚$  $@‚ýAFYH°koñdQáË $@‚‘` Á@‚$ f!Á ö$9! $ Œ æGô%$Œ³XH0 ÁH0øR#ÁìŒ F‚Y,$ `$@‚$Øaì÷ÄÖÛÑZ[ëïÛJLj ÁH0 æû™H0 ÁlW _[bkÖ:@‚‘`Ì÷3 `$p Ù–Xû:ÎŒãí!ûzÚî‹+ŸÇìr^·û}œ‹FŒmEˆ‘` ÁH0‹…#ÁŒD‚M,w”`£õÙó<Ί $ÁÖ–`­¤ø$H0ÌbñýM‚$ ‰ño V’'¯Û{#ŸÞËÙ:NíøïÛKuÜÚ·%†ZçxZñ<2çÔ[—Vù~DÏ—`µH. $ f±`$@‚‘` ”S-Á3#:¨W0eŽ·÷¹E¤àÙç‘9§ˆpëY÷#z^büZüÒÓK‰ñ# ól] Ù–Xû:ÎŒãí!ûzÚîß{ÞuÏcv9¯Û}Ÿ+Á¾>Ÿ_ëgˆ†Ìv€#Á@‚]:ìL fŠD“í}nW8ÞsšU¦Ñã‘`3ä€ßL ÁD‚Í>w’`£õÙó<Ί $ÁÖ`+‹& $ Œ; œZUkŠÞl¥ó Áž)Áj‘d v¿H°’ `kG‚)Á¢ÑL‘h²½Ïí çÑ{N³Êôý}Œ‹ˆ¦YBj†ø"Á@‚‘`À’_ÂQzDVMôìU®‹Ñ$@‚Ý_‚eÊlMÝ-ÿ¨¶Ûûh®²hÒ|‰ñ×NŒŸ‘`½‰ñ[Ió%Æ F‚$@‚‘` ¶d:ƒ¿—£Ê±\wñýÝ/ÁŽ”¾@‚‘`€Äø$H0 Án f±`Ï“`"²@‚‘` 6ð?ÉÀ1|ø=A‚í%Ȉ7`$˜ÅB‚]/ ÁH0€û_}¿ÿäǧxø¼A`/7Ý¿µG´Íþ`?>?—î'ëJ0" $ f±`$@‚‘`À%Ø·_"ìOLçï·¿ãóóøÍý$'ÁÞåÔ»¨Ú’W¿çòù¯@+mŒ³XH0 ÁH0àr Ç||.°§ÛR%aU’W%¡–=@‚‘` $@‚‘`·¾‰#…( 8\‚Õ¢¾jQd$H0Ìb!ÁH0€#ÁðÛMü/ø5ДïèA¬´œ –#Á@‚‘` F‚$ ¬S‚yò ` F‚YH0 F‚‘`7—`~ <{$†³ã×¶·ò€y:$H0,"Cfî·G]Dì®lo¹öú{a•:$H0 =!º •`H°«I°³Î#Áî"ÁŽL¥²H.`$ ¶ëï•dY­N{ÖÓŒ `{K°××Þ×Kòi뵯õ׿3ï/É®RýZ¢ªuœÚë­ú[H°³$ØVdU)ÚêuûVTVä8¯ïϯZ}¶ê°UN­ìž(3€òìÁ”9þHFÎÁ„g °ží `{J°ìzDRÍZÏFp½Kº=Ê´`{J°–lª ®’ôЬGDX¯«‰¹RÙ­óH0ÜF‚mEc}ýûõµÒ~ïÇŽìWªG¤Ž™2H0læþ `gH°LÄX-òl–š!¾H0lµH°RDÔ^lD8ÕÊ<«N ¶q“y ßòme V[/‰°Ìqöدõ^Œ#Â@‚$Ø]%Ø,ÙD‚‘`¾¿c¢¨' ëáÔ’x$H°$ØÓ’¯ÿäǧ[9'Øÿ—£_𵢏jedYTŒ‘`H0€;[‚e"¶H0 F‚‘` ÁH° K°Ã.t4,A5[‚Eòze¢¿¢Ç$Á` vÖÓ!K²¨'1~i¿È±3 ó£Op¬ÕKb|ìj9Á2Iâk‰î³‰ñ{„[­¾#Lb|`$Ø¥ž@8c:ä™,éE‚Á”G` ¶Bÿß"“X¾w‹…› †¿E‚#Áî)ÁJù¿F¦:FÊ‹&ѯå'ËîC‚ `gE‚e—) F‚-*D;@Òì%ÇzdË%H0L;` Ù–X[UÐý>ؿ湼Öæõºz»´êõºý©Ìb!ÁH0€#Áv—`½åõÖ#Á@‚;[d­(H"rä LWo—³Ú›³XH0 ÁH°ÊÓ [ÛkO=ì} ákùÚI0 $0 VŠ$zÝž Ê–³uü÷òß×·¢¡¢¢¥´íýXïuŽœO´Í¢çR*»T×hVi—‘úmµIkÿ­òI0‹…#Áìñ¬7Š«'Q{扄™È0Œ ,"§j2c$b'RNtÿÙuŒJ–Zy½uj•-çÊí22í²·œ;I°RR|à\>üÆ%Áìî,“Ø=ûTÂÞ~ùìÿÉOñðy.ˆñÏïÖ—Ïfµë?ìÇççÒ÷3‘ ô7•`½O4ì9fF„‘`$lu v¤@ÉÔñÉìÈv!ÁÆοֿýa"ÅÿK0m1ÏÏã7ßÏ"Á€µžš¿GL­Þ.ÙDêþÄÄø Á@‚L¤’H°å¯ &1>pÆB̈œ:¢œ;0;jm´+¶Ë u!Áž-Àˆ0` F‚‘`Ú— †G‚a_©ÒŠ;£+¶ †•$ìwaÙž=Îì²þ)5÷|g´áUDç¬k¾jù$ F‚É  $@Z^àûX°c ¬.öªë¨üxݯç=GJ+Fþ-óH0’†sm@‚$ð F€'Àˆ0\A‚ýžg:¶}K•Þ•`Ñò¢ï‰–s­2KÛ¢e•Ê,ïõ}Ù¶}­ín‘Ï@뺜]> FÒ`® NXï?Ž·(¥mÚ$@‚`$0"Á2B""1ZòfvyÑ2³ë#§$÷f´i­¬žh¬¨mï#Ë'ÁH̵ÁŽ‘Y%ñUbÙ}I2x:/àiÍØ:ŒÕ%X4r§ö¾žè§Œ8k½§%…fI½^ –9Ïì5ÛCÍ_g—O‚‘4$˜kƒQy½ V“3ÊŸíï?ùñ9˜ÿ¼·`]¾îÿXåzü#À~|ö!÷ŒÃU%XV D%CI‚!—fLÃ;J‚Í’g$ æÊ/ö¿›$ vդ‘×{¢¿ö¬ðöyùöK„ý‰åù[,Çgßùæ^"R¶öY ¶ž›Q> F‚‰»™h!ÁH0Ì‹+½7*ÁD‚пý˜¿4µ.’„=rÜV]"²¬öžÑDé£ç•MÂßJòˆ#SD£OPŒ¶W«^Ù2f—O‚‘4$ †s‚½‹°V´Ö–Ì’ €¾Ÿ íôF*ÎŒŠy Ú $˜œ`$ ?´¸ÀgÚŸ 팤!Á\ø¡ À½>Ðþ¸¥@‚‘4$˜k?´¸Àg®H0$ F‚¹6ðC€{ý$@‚‘`D‹ö%Áà‡6÷@H0€#ÁˆíK‚Ámî€þ` F‚-$ 0è†kïš$@‚‘`$H0€A·ë× ÁŒ»‘C† ºášÃµ vº ©ËèyÌj‡½Ûóu,·JÎj‡ÕWk; F‚‰[àÚ``Ð ×®H°3eÁ»(8C‚ôïÈö‹ˆ–Õ¥ÐYu8«>$ F‚‘`ðC€{ý–`ï‘Hµè¤×í[ûE¤X¤¼÷ãlÕçµ5aSzïÞõÈÈÂH¶Ê©•ÝºŽ¥s¨cKFµ¢Ú¢VÑÏMo[o;ò"ÁH0LN0ø¡ À½€þˆ‹J°ÖþÁ• kÉ”ÈzM ®ÖcF$]IRö¶Áž×m$R+{NÙ2"â²g#Z´/ ?´¸ú#pq ‰ZA‚í¹~„üéiÓYuªEÒµÄ[+ÊoT‚e¯M¦Œ™ŸŒhY²}g_£U¯9 æ‡6÷¸¾p½à3¸wN¯¬˜ ÁÆò])ÁŽŒþ"ÁH0ìD –)ç_³M‚Ámîpmášë:$ص%X4G F‚‘`íûÚÎïë%)µõZt[Iv•êA‚Ámî8⺾nßçë·KkôG`™Äñ%éIðÞ›¬=“¾7ñùõÈ&ÁŸ-#‰þkõj=| Ó®ÙÄø™r£r,úY•¿q“yKI°ÌzTh—tcäÖþYïÙ×gJÄ}>ƒQz£vF8«ÜUëqô¹>é¼hW‘`‰»º+Eƒ•ö!ÁÝØs šy½ V Ï(ú#î F‚ÝSÐÌœ®Œ»¨ˈ²lô 0èFT~¼Þýµg ?‚H0LN°$X42‹ó º±òµ+½7*ÁD‚é Á`$ØÂO‡,E…eãG§:fæ“`ðC€{z¯M6G׫kEkmÉ,9ÁôG`H°Ó$Øl¡²§ 9S‚}®%üÐpÅDǵíùnH0€#ÁdÏž‘J$ ?´œ8°Iüq Àà»$lלU£Óõ¢ÛJ²kïœU«I0`~hk€#Á®šL;ùnH0€»Ë&eÏ&nÏ—#Áà‡6€{‹0÷÷h¸î Á`—`Ù¤í$Î’`#9jüñý'ßü` F†ÀuÎ’`‘ýg‰µÙ‚îß±ðücœÏQuÙ»œß}ÃuZéZ‘`“$XF”e£Ç®*Ááãp æ?€<¿úìOæ;f‹0ƒp"®=H°ÅË̺­ñ¶WÝŽ<ç£ßYד› Á¢‘Y$X¸}¿ÿo øññYîY|÷Êü`‡ªI0`D‚}˜¾ŒéÌ|M" \!¬Uõº=T’ [Ç)mÛªçû¾¥cedN©ìVÝ2‘SÑóÞŠt‹žs«ýzÚ¢U‡Òuê9Ö*׊ L×+E…eãG§:fæ_X‚}û%Âþ<™¿¨C„C§W‘`@¿“Ã{ˆ0ðõDF‚W’`5™19Ó:F­¼VfÕ7*àzËÌžwVüdÚ¯7Jª%ÞF>O+]+l gÕÝrZ=uàæ `X]„ù®ZïwCD€‘d~3«E‚­&Á"ǘUß2%uõߘùläÚE"ªfÕiµkE‚‘`$˜/U, 62ÛwÈ15TÖõô!Á@‚=õÚÔäVëuøÍ`÷”`³¢ÆV¹v$ æ „~Ð`’7·í-àzË#ÁH0\W„ùžZû7Ãûÿôk'¿Œ#ÁH0Œóƒ†{„+mÛ’YÙ÷G±uœÖ1#už™F‚$ئBº6$p' 6S¦d»÷ȘlbüHRøl™­s®Õ5Ó=ûg“àÏ”‘ïÇÏ^=® F‚‘`~Ð`QO¥¿#²*²_VvE£ÅzËŒîkI°÷Èi¾]ùûXŒïWîS«ößÑ4G•3ZÇ'—¿:WnŸGK°çñA‚`[ª'²+*ž"åE"ÏFE®'Á\+ ;*øÇç ýÚßïËËúÔÊ‘`w+œëC‚í'Á¾ÿï¦üññÙGóÕðÇ•÷{Ü— f¼G$X-wVV*ÍS¥é³Ê 1•ÏŸoøOŒ·çßÿŽ´ GÚDeIDATÇñí¶|ª(rÚÙ 0HÖ¿—Êؚʜ™²yØÑ®“`q VÚö.¡J‘]µ÷oý»$â"Ç!Á°ú¸æ«ì÷¿1ÿíåöÔn$´³A2`¬_&ºsÆÃ+JbÌÃ+ôo쯪È*‰­ˆ¨Yï)› L»‘`ÐÎÉ€A²þ}Éé3–1ãI± ÁH°zî®V$Wäý­ã’`$ØÝÆ5¥r³æˆ"L»‘`ÐÎÉ€A²þ}‰éÙH°Ú´È‘2A‚‘`1É”‘`½IùI0ì)ã)㬹2‡Ón$´³A2`¬ßJ‚EE ¦“`±¤ò£‹#Á06–2Öš'rH0íF‚A;ïÜ.zù ÁäkNgÌ&ÆÊ.‰ñI0,.¸ZÓ!#‰ñ£òJb|ì©ã(ã­9"‡Ón$´³A2°¨!ÁŒ{Š\³`ÆQùm s´ FÎhgƒd€Ó¿Œ[LÒ”"¸,$Ø“Ç5Ù<`Æ\sD¦ÝH0hgƒdÀ YÿôoÌB‚ùýN‚‘9Úsƒv6H ’õo€#Á,$˜qÍþåô¾&sÚíÙíæ&íl $ëß F‚YH0ãc+×T»Ýÿ<]8hçãɵ'”õ$65$“` F‚YH0ãc+×T»‘`.´ónlæ¾{‰7ƒ$“` f±`Æ5pMµ æÂé ’·¢±Þßóúz)z«ÑUV[ÇØ«À]%€^>./Á} 0VÕn$˜ §ƒ (ÁJÒ©¡U“W#Ñ\gÔ <³ßA‚‰?²¬ F‚ƪÚó×Η—`QáÔ3ÏF‚íQ€H0Ìb!ÁcUíF‚¹pÚ™KåïÍëuF пI0‹EN0[iOíF‚¹pÚ™;L‚e#½H0$“` f±`Æ5ÆV®©v#Á\8Óã—’ÍGö‰«‘ãgê`H0‹…3®1¶ÒžÚsá´óÍÛe ƒ à>lFŸÞã¾°W½ÜÃ@‚õI°×÷ì!ѶŽIÖ‘` Á =µ í,R0H&Áv:†ûH°¸¤š±ïÑï±`Æ5ÆV®©v#Á\8D;“`Àƒ%ØV”gkÊq4/_騵H­Z}²Ó¦#Õhµð$ ö{?ˆE‚½î_zK\E"Á2åXH0ãc+×T»‘`.´3 $#Á"2)óà‰H¾Àì{gN‡4]$X|Úcd}vT×åXH0㸦ÚsátíL‚$X(¬'z«õÞ§¾ÎÈ 6£èßO–`‘ÏÊ 6£ «j7Ì…ÓA´3 `ÝQY¥éÙȲÙQZÑc̈:ƒþýd 6#q~4Òkïý 0VÕn$˜ §ƒ`H°¡é$ôoŒ³`€±ªv#Á\8íL‚ÉË%ÆoMgÌ&ÆÏL{l•—IŒÿ~¼ÈqÝû 1~<1~)9}Obü÷²Gʱ`0¶ÒžÚsá I0À YÿH°¿· •,$˜q±•kªÝH0N!ÁvLd÷¯E®ìU.@‚xÚtH‹…3®1¶rMµ æÂé $ØÉl†Ì"@‚é f±`Æ5ÆV®©v#Á\8ÍAr$Pd{$qvT‚Í( Á` f\clåšj7Ì…ÓAH°î§»eŸ—Û»\€@‚Y,$˜q±•öÔn$˜ §I°C%Xæis$ ’I0€‹%ÆÎãƒ3®kªÝH0xíL‚õL“$Á`L‚$˜H0Ëu"ÁH0ÀXU»‘`>ðÚ™#Á`L‚ú7 f!ÁcUm¡ÝH0hg‰ñç— `H0‹EN0ÀXU»‘`.œòÀvÙ‚Dž)ÁJ¢yï¶Ì q€#Á,$Œ­´§v#Án˜œÔ‡[;¯0Àý%Øj}Þ=ú÷õ%g!Á<ÚS»=4ìóeþèÚÙt)À yÏþýùYšV\‹+MO®åókM]n• èß×’`D˜…#Á =µ FÎhg 0H^*Ò³%¯z#¸¶¦X{¸ôïc$Øëß[ë5Yõ~Œ×ý2ïf!ÁH0hOíöМ`QA£hg 0H¾ª›%¾ÜŸ Ï‘`¥õ–¬z[³Ö-$H0hOíF‚‘3Ú™ ’I0 ú÷.‘`ÙU“^£ï'ÁH0ŒƒöÔn}:dKÐèÚ™ ’I0 ú÷‘,’Ô~äý$ F‚øÃ5Õn$1£I0À y‰Äø51–MŒ9žÄøÐ¿I0 €Ón`5AãïI0À yfÿÞ¢÷|]wèß÷”`¥„÷[⪕?’0ßB‚ Á´ÛÃ$Ø– ñÁ×ΫGŠ5Mik°nŠ ’—Ü"² __‚í)5¨ 9Ú#g´óm$Xïk3"R`,Òпג`¥h/‹…# =µ –4>ôÚyµAr6«-Ò“K¨•°{FÙ €³XH0òÁ5Õn$9C‚‘`©'ÅÍž¶˜‘`¦L ™H0ÌB‚ø“®©v#Á‰ùþ“ow’`¥ÜA=ƒðLÚì² f±`þäƒöÔn—–`ÿõ°¿dÍOvÇH°9¼z#ÁäƒA2 `$˜…3ð'\SíF‚‘/XJ‚},ó#€H0$˜ÅB‚ø“ÚS»‘`À.l¥3$Xm cÏ&{ãgÊH0$˜ÅB‚øÃ5Õn$ð0 ¶…A@‚ÝžÙû{H0ÌB‚dŽv#Á&ÁJç÷º}6x°Dë„kE‚Xkœý¢¿JÿÎD…$Ø>,²ÿ,±öuœÇÛCöõ´Ý¿ÿºç1»œ×í$@æh7lºŒjmÛS¾eËz²$#Áƒä‘›³¦;G¦¿ï¿µ>2 º´­ô”Ø­º—Χ7Ê ú·H°ýË%Øh}ö<³¢I0€ÌÑn$Øilk½ôw혥ý[Rìõx¥²#Ä F‚W—`_"2¬%£jâhK2•DXÏC+zäXIŠíU'èßO+É“×í½‘Oïål§vü÷í¥:níÛC­óŠF<­x™sê­K«|Œ|€öÔn$ØÔˆ¬–Ôª ²–0kí×z_+ìI‘a$`·+œGï9Í*“Ûoà_Zý_ÛÖóÒ†ÌÑn$ –ŒþŠ$ó'ÁD‚ë?Íp V’a$ ìŠ,SfkêæhùGµÝÞçA‚Ýk`¿%±2r«¶^úó–p#uÈíF‚òtÈìú, –ô"ÁH0€›/ÁÞeØŒéÙ<[­²³‰ë£Rl¤N v} 6:½r ¶×y`×è³µ|ŸïßÍ[ï)‰¯­ýßßS*+¾t¬Ñú9ÚA»‘`COZì•`­ÄøïûD„Wï>$ ¬-ÁŽ'ó§tŒ'_3Ÿ]`M“C£",’8~k`6¡üÈÌhTÖÊçÍUMš/1þ>ò+óz+:+òzM"d¢ÃFÎ$˜v#Á@‚‘`€Aò!‘`O”•>·xš‹ò×àrT9–ë.$Øþ‘`­(®- ‰ìªEЉ#s´ `"4`|‰œ`D‚Y,$Øõs‚µUKf•Ö#Ç''ÈíF‚$Ø`´†Á ’û$XöéH0‹…ÈíF‚”`¯o­×dU)qvöý"Â`œ—`­i®@‚Y,$@æh7 ÁÞ$Xi=ò$¶è~Ù§ºÉÛ,’cCH0‹…ù =µ `…H°ˆìªI¯Ñ÷´Ã ¹?i. `=‰ñcø Án4ð'm´§v#Á€GH°HRû‘÷´Ã yÞ½þ?—î$´§v;@‚þ'Œž6HÖŸ€GK°o¿DØŸÀ"|~¿¸“`ОÚM$ vU VJx¿5Ðn%Æ$Ì ’÷•`G÷»¨Hôo Á\Sh7 $Øå"E na¼fÿ^©oºO@ÿÌ5ÕÚƒœ`—”`¥h/À yþÝ‘YŠø¬Mm®½î^ý üášj7 $˜œA€Aòá‘`=÷‚Ò1²9݇ Èí ÝH0`$`|ØtÈ6"¾Ü‡ Èí Ý’`_S1¾Ö_·ï)B"Û@‚‘`€Aò‘9Á²÷ ú7dŽv»ˆë}m ¶Ç{@‚‘` F‚$ÚS»=\‚½&äÝMïï{ß·µ½&®"‘`ïjµr@‚‘`€AòhN°Þ§I–¦VJŒý<ÚS»- b3¦L¶$˜é’$ $Ïèß[¸~ ÀÀŸ|pMµ V•]¥h¬YS[Ç%ÁH0 0HÖ¿ý€¿:¸¦ Á‹›‘¯+éuT¢~`3 ­íGÈ þaL‚$òÁ5Õn$ lxÉ'tæÔ+Ó¾`L‚$òÁ5Õn$X‡«%ÒïyÂäÖôÊÈq‰0lÏAr)gÐû¶­ý¢ï+=Q®ô„¹RbíÚ~ž:ƒd ÁøÃ5Õn•`[2 Áþ$G$RM.E¥VK¬EÊmI. É$@‚Èí& ÁÒ¬ù5*ÁZÓ+£R+ú^€@‚ÈíF‚$XJ"­$ÁzöH0$€ÌÑn$@‚uK°Ñé”$ ’I0@ÿòA{B»‘` ÁNMŒß3í°W‚µã·žéé0HÞ–Üg´“Ï ôoðtHhOíF‚[2RÄ ¸o$ØŠýÛ=ú7`®©vÐn$H°C#EDO÷ŽkE{–òüm­GEÖVh$п€+•Çñý'ßH0 $˜œA€Arwοȓa·Ö{£¼JÇq‚þ $Xn|ïËê)üúýøãS„‘`$H0 0H–`[ÑX³6±Ç1ý F‚áIìcó;™›$ÁJûŸ!ÓVª H0÷ŽÛ㉫$ôo‰È+}'“`;K°ÕD ¡D‚‘`€A2 `@‚#Á†$Ø%ñôµ^;öÖk­cÖ^¯Õ $ $G$XkJd¯°•iîEп{I°ßÇÑÃù©þýˆr÷~¯Ç,?² vÃH°ÚzD„E%X´L‘`$ $ë߀þ $ر©W¾Öã ö^Æ aH‚ÝP‚õ ©â‹#Á ’ƒdý Á@bü~T@%QT“Z­}ZÇ+Õ©T¿Lý[ulI°­õÒß=õ"ÁH0Œ[vÙÖÀ{ÕüÖÔ±«Ëkýg^¯«·Kvj `àé×”`-AÕŠä•]Ñh±h9™ú÷H­š ËÖ‹#ÁH0춃ä‘ã¯8€ÏæCº’à¹R»œÕÞ$@‚ v T3Þߊúª‰´×í#ué‘`µ6޾»hbüšêIš/1> v¥H°VÒížÈ l9[Ç/ÿ}}+**ZJÛJIÅkOç‹«ÅÕÚ¶Uv-z¤«´ËHý¶Ú¤'q< `@‚‘`{¬·¦fÚ“`“`QH`99U“#;‘r¢ûÏ®cT²ÔÊë­S«ìl9Wn—‘i—½å` $Øu$زè, 6úTǽêH‚Ý  ÁrZGH°l„NT͈TËþ{¤Ý²,Ú†³®Ý‘íÒûÙU& `@‚­/Á"S {$Ntzc+~&ÇV6aïÌ‘vˆ´+ F‚#Á&–³g½"u|²;²]H0 Á`Ï<ÿ•¿DV¯ßÕ ÁH0`$ F‚‘`$@‚ ¶¨é}ú!H0 $Ø$Ø ™™Í³Õ›¾ôž¢)“¸>š¿GL­Þ.ÙDêþäÄø³q´ÌÈg ÁWŠ F‚‹K°(3½{–s—ØÙB`Åk±Rî*ÁŽº³Â`@‚#ÁH0`¦K]hÐÕŠ;3:g¥v1HžÓ¿³‘‡¥k‘ÌDΨ@‚À³ãƒ#ÁŒ,Ázó®Ížî|f E‚#Á N=÷‘l$ØuH0$ Œ»¡tÉl €ËæïÍëuF ÀEWïkw’`x$ ¶ŸƒÄøÆ¾I°±Äø$H0(ð³ÛE‚áê‘`$Øìk€ÙV[ß9fKTµêP{ýuŒûÕ?¾ÿäÇçå³^büú2þñه”l>²ODX?S€@‚‘` ÁH°Nñ]/½Özo¶~%á–YÇ£%Ø·_"ìOi>ûη•uº$ F‚ Á<ô»ý‹•“õŸ)Á¶"¤ö`™rfåký›#ÁH0`H0$ì!Ó!Ž›•{,éuF~3ŒH0$Ø/YÝÿ•£ÊH0ì%"€óý'ßî&ÁF"°H0ŒH0$XqÿÞ:’` ÁpöýUgÁ“ù%Á~|а•§CF¢¸Z‰ôk2ªôÞÞÄø­z?1/ F‚€a V‹ÆÊl/•Ýþúïå$H0à( öqÊ}²™æº`$  Ö”Q³ÖG"Áö. Á@‚Lj ý$ `H°JN0 $H0€Óÿ@‚$Áþ ©¬ï³µÞÚ7²}o ³J]H0?"<;Œ ÌwÆt¶R%` ÁH°NI´‡[MÎT$Œ æ»áG nW:H0`“`¿ÿOD]ve%ØVtÙÖ1jÇÚz­uÌÚ뵺 1þÞ‰ñg— `0¿‡«=±oë‰y‘§ÿ¹n$H0, šfI°ÈzD„Eë;[äp½AÚ$@‚Á |e VZ¥)%YÖÚ$H0ìd Ö+¤fˆ/ŒðÌéH0„_E‚Eä F‚#ÁH0Œ@‚ Á`N‚ F‚Ŧ’` Á` ÷We?F‚‘`gõ¿>#QAü¤{ vÂÓ!3’i41~íx=Ió%Æ'Á`H0`«‚KIð{å–Äø$Ø]ž,Úúlßõ©©$Ø e†à˜}L`031>€¾«A‚=쉔¸‡«IÕÞ'‹¶žRZÛ¿7ʱV¿h”ö¯•Ñs.ѨÌaM‚ `D‚ÏŽ#Á@‚Ý;Ò ÷”`GL¥>¥´V¿Òß‘úeDXë<{Ú¨'rmäº`$H0 Á $@‚$Áz§+Î`Ù²z…T¯@ž%¾H0 $`€œ` ÁlQ Ml 6:í˜#Á ` Áì¡,ú ‡=$ØQOR%ÁH0€pi ö–ˆlë9@‚ž™?›ƒ+“+l–U#ONmM‡ìIš/1> ÌIËI°×÷­ Žf ¯3EH0` ¶ïÓ¯W{2h¶ŽG”“Ía¶úg#Á@‚À¦+Es½o{߯&JÇ«­o••9fiÛ{·ÎKDH0ì=žÅÇ’‘`«H°eÒÛ…#Á@‚@J‚EdPMµ$Tt½%¢2Ò*ó:  "Á€=¢®#ÁpϨCl¢û7Œ°¾ÏÖúSDNf;H0kJ°VÔTV*еõ!Á@‚Á  Á@‚‘`ƒ‚‡›×v$˜ÖÖŠ‹äà_=S³ûFË#Á@‚Á ¸ob|`$XBÌüžL®.»²l+º¬´­¶þ¾-rÌ–¨jÕ¡öz©Ýp &×äûO¾„­-ÁZÑ\$H0` ¦ÿ[0l†k‰§èz«ÌÞè´Ò9ï!I°ëI0× +õŸŸüøaasã—¤QéõÚÓ[ò-’¿5m³•`¿•< Á`` ÁH°$،㶢±Þ#²zÏ{¤^„ ì/Á> L;$˜((€úƒð»ë€ ÁH0`7‘`³SdZ# F‚‘`ÐH°Z4 ç èA‚‘` Á–”`Ñ\a³$Xkú# fO‚AÿA6 †¹ƒð­(®Ò¶wéõû ‘a Áî&ÁJ}œ'Á–:dFE“â·¶Gʨ%Æ/Õ©'i¾Äø$bÒ „õ Áð ¶5X}—\[b«´0H°ûH°R=õqì eæ ˜ vÿkÙ+½´+ F‚$ž'ÁZëÉ ÁÆ$X©oÖ¢.·$u6:3Ú·êñ^çÚk£Qf$˜H0À ^ÿ™"Á"9üÞ§:×ö F‚$ž+Áˆ0`ý¬Ö?[š­hÍl¿ŒÈ±’Û«N$ Äë?»J°ÈtIíL‚‘` Œ 6', ]‰ºjMy®ý{äB‚‘`€A< vj$Øž¶Ð@‚$®‘?:P×A‚Í“`³Ögä#ÁH0 ñ$Øí%X4‡˜ö&ÁH0€õ$@‚‘`$ F‚$Ø£ã“`úÏ$Øë€È¾¥õè1ö’‘m=Ç  Á®52›g«52›¸>*ÅFêD‚‘`€A< 6ýZ–’Ù¿ ¯Þ}@‚!ÁÞß—9Ζ[AÍ^gŠ<` Áìýo‹'_ Œ3ˆ‡A< ýÇ -‰õ¾mk¿VUk½&JuŠ/rNÑH°×÷µÚ#Áàþª³€[/ìIíÿùG‚‘`0ˆ'Á ÿ˜¦$XFXE#ÁFÙ{ŒÇìy#Áƒp€ vy 6c€¿‡$ȳ6-ëJçaO‚$ر,ù•‰ŠÊÈ´½ä\O4ÛH}A‚ˆ ý$ v°­ fO‚Aÿyn$X6'Ø‘Ñ_½õêÙ7Z F‚ÁýUg1A‚»¤ÛŠ j%¼Ž>®tìÚÓãj]‘í[å¯vñ$@‚ÝC‚eófes‰‘` Á@‚$˜þç³F‚í –LÕJ‘ýgM‡¼ÂyÄ“` vNbüá•l¥2þª$˜þC‚Ï•`Oùƒp€ váÄøÀy|ÜF‚mEPEžVšy*éÖë­'¡fŸ®}*ê çA‚‘`w`î Á¾îÕî7$@Lè ÁD‚Eqôq»H°¬`Ѝ–PŠì?k:ä΃ÃÕ$†«J0ò á &H0`$@‚UÅR&êé¿y4údÓètÈ+ †’{ÿ»$¡^÷ÛZ/½·tœV9"ÂpE F~Á  Á@‚‘`ÀÃs‚õJÒ´Àž©‹³%ØÕÎCÿAM‚•ÄSIX½¯×Þ[“i‘2ý¾Ã$˜è/„$H0 Á¦È£Ñé•«H°3ÏCÿA+ì]veäXt}KªEËV”`ä  $Ø%%جe4Òx‚k%Žßš˜M(•D#‰ñ¯tú 6C‚µ’ÕÏ’`™r€ÕrÖ‘_0H0`$XaÀj€Š»J°-\K<­ÿ¼~ɾán –W@“`Àp$Øæw`ô»Ò~÷ÞOg1A‚»”k%±ŽD‹˜º„'G‚Wï?­/Î’øª ±ì¾{~‘—$Ø{¤K6oWéÉŽÑrþòtHˆƒH0€›üŸô¸3$بÛ#gY X¿ÿ´·¥×[‘`µg3Ê‘`äÃ5$ØŒã–"ÕJÇ&ôpA öý'?>ÅÈgpo~ °Ÿ×ÛI‚e"¾H0`À5úOk y½'úkÏ:‘`ÀþÌÓ!±§›- ^÷~l² 7’`ß~‰°?ñ>¯÷7l 6k Á€{F‚ÕrÆD¾G"Á²_ô$0O‚=í7âl+òªõµ}+¿Xä8¯ïoI®­z¶Þ'çV—` F‚#Á ÿìœì]„µ¢C¶dÖŒœ`™h ˜/ÁD…áU‚EÄSTREßû.Ò2¬§Ž ìÆ‰ñß§C¾K®Z2}T`€þstRX 8^‚‘aˆDoµ¢¼F$Xd‹¾ ì& 0ˆï›NåºAÿ¹æ—? ì+ÁL‘$Á2ÑTW‘`DH0`$p+ –NåºAÿ¹fT #Á@‚­ Áz_#Á@‚#Á€[K°¯?ÑéT­ó9²•ÊÒÏõüW†‘` Ö;˜ØÊ‰ÙöÄœ`µø™D÷ÙÄø5•­“¤ø Á@‚‘`Àí%Ø« ëé?+õ-ý\ÿÁvħßéÀ³%Xæ‘õž}ï8`¹“("½@‚#Á€GI°ZTX-ò*û ˆÒƒ(Jÿ.?RÜ·ÿlQ“@OÞïK‚í%Ãf·ç8ïï)I?"O”`­Ô¥×[‘`µûÎŒòI°ã% $ EØ-%˜Á»Aür‚õ>M²4µRb|`ýytZB'…U‹²Š ´Ñ译œ`³ë îÁ: ÒI0÷Ñ¿n)Á à âW`™§Î¹– ÁÖ•_GK°‘DõGI°Ùu ƒPr‚¹OºÿàˆþÕ-ÁJøÒT¬LdJᆭsį ÁZÉk]7è?×Lþ™¹•Cl41~Fzµ’ôG…VæµV 0Ì€ ¡ æþƒËJ°’|êÍ9Ô»/ f¿‚‹N§rÝ ÿ\÷GÒ,!PÊ¡Õs¬#Æ™ï †gBg PKÇøý?õÏ ­r¼ž÷¾¿§Ô¦{^ËÕŽI‚‘`É|UßÃ×cd޵U~äýÙúžÙ·H°±ÕŠ0kå3"Á âWzl: ý‡[UN` Á@‚ý±Ìm¥ˆ‰‘÷œu-gs¥A:ž!Áf•Ý{œÈ½‘;@‚e“Þ›i¶ótUè?$ s¡¥ˆ„RdQdûV¤Rm{íøµ¨Òþ­úG˶C&2+û¾VÝj-­ó¹–³Úÿ¬ºûäó$X«Üèç9ò9Î~ÖGî…µí$ †â}.¡ÿ` lB[¤4€k ì¢ûf]ï­ö8½ÇžÙŽÑuotÞ‘íO‚‘`gI°ÑûÀ^}5{¬•¢mIŒ?šì¾•X?sLÄ“` F‚$®)ÁjƒÁHtC42!{üÑýg's¼h$\©gK°×rVû“`$ØÊl¡?Sü¯e¹[$`O‚Aÿ!Á`8R‚E–™é½‘ #ûÏ.73ðœq¬#£K¢×’ ¶F$, `®ô Á€$Xï€[O‚Ýþ$ v¤;B‚-¾n!Áƒøùìu:ïQ9îzòóAÿ!Á`821~tÊM61~6‘{$‰û¬úg“šÔŠ´šuŽÙ¶‹~² Â[íâéî?g]ûìC(²ä¾5òUû ƒøE$Xïk{H°=Þý‡H0÷¬8ùD7h/ÌýG\»‘`0ˆ_@‚•Q{êjöÁ™íµòJÅ€þC‚Å…À‘}Kë[ÇØ[Ôm¿§LB‘H0n`î?çEš‘`$ â—‹‹±S&[ÌtIýclDTÕ$ØBi–ðŠJ@`0H0¸ÿ€.ÁJÑX³¦6¶Žë> ÿ<]‚Õ"±Zûµ¢©f¯—êX’s­×"’ïýX+<`0H0¸ÿè_$ â/ 6#_W4Òë¨DýÐV—`#bj¦kM‡|—Q‘H³š8ËÖ=rþ$ vçûÅÖú׿#Û B î? ÁŒ ¶¬‹D~e¢£öXF¨µÎ7*ùH0Üå^Ó’T%ñUbÙ}3õ1H0¸ÿ`! 6s:×~OÇ3ˆ_U‚Õé÷ƒr‚M‹}òÝ QÖ#Á¢Ç%Á âI0è?ÏŽ@‚a<’îuúûtÿWˆm­¿î¿õÞ-ÙU:&@‚‘` Á.+Á"Q]$˜A< ýÇ. Áp^TØ«ÀŠŠ¯šüê9ŽÎ Œ 6œ?’ï«GŽ•¦]f’Þ—ÞWzZ_+ù? â=}WR H0€Ã2l†¼šµ`$Øêù³ï?kÊù-%Ø¢¡ˆ*L$Pî?$ `Xá?I0`î“"ÁŽ“`OzpÈÔé«GC$ `$p¾Û"-m¿ûï•`Ù\a$H0ŒH°Ò½©Y5rê-#"®"‘`[ÓËïroÜ% Áôè?$‘`‰‹$²ïÍVPê¨ ÁH°Èd÷Zß[Ü·$ØÝ§K’`0ˆ'ÁŒH0,—lÅÜ; &lKªï‰š-cVN°Ö¿I0ƒGÄ"Á"ûÏꓵL¬0¹§íF§l¯p³Ëyˆ\ÇK°×Ïæûö½EDd[Ïq 3žI‚sŸ\]‚µö›!Á2eÌLŒ×iâ$H°G‚Í>w’`£õÙó<κ—’`ÇK°šì:R(Í^[ Á¹‡Þ)15@‚‘`™|_$ `"çý3Vñ^ÎÖqjÇß^ªãÖ¾-1Ô:¯hÄÓŠç‘9§Þº´Ê÷ƒ«O‚Õ¢¹ZûeÖKÇ/‰µÖkA÷~¬H} Ùè/ Á®&ÁZy Góö–h­§ñÞ9/ ìÂlK²ÌŽxÊJš–°‰®Ï>·ˆ<û<2çn=ë~på%X¯ÐÊL‡|—Q­(­ÚþQ •d$H0Ì’_$@‚­ô€ŽOEt?#Álr$Ø™,͉&ÛûÜ®p½ç4«L?¸æK°VôÔ^-"¾²’‹ †=¦>’` v•H0ŒÛ]‚í‘4|…Õ Áî Á2e¶¦nŽ–TÛí}$Ø}#ÁzDÒ¬õ =.  +B î?8]‚­ø”µÑ'Ç#Áú§^I‚íu$ ¶—‹Du‘` Á` `î?î?$Xbßš*”ÉÍMž-wd@ ìjl†‹$Ž/õñL÷‘)˜Ñ¨¬•Ï#š«,š4_büµãGò}õȱҴËLÒûÒûJO}l%ÿH0„$˜ûn/Áöˆx˜•`:óD=Œ[!ñâv2Æñ¶9¢è?O”`Wˆ†"ª@‚áiƒÐI­WÌ tt’îÒéö8Ï=®Õ¥†û$ †‹K°hÒéÙS²jbÙ§ÂÁ ~…H0@ÿYs:äêÑPH0üà"Á ËK°ÞȯÌ1F÷Ÿ}œÌñ²íÑS·ŒàŠFeE¢Éf L ³$ØŸ±l9{åñ{¼뤒œ–`ßòãS<|Ö¿óϽàeÀ„½ÛûöãóséG `¸B$X¯ôÉF*ì?»ÜÌ€{ƱF%XϹµ®% ì]÷¿³$›:’À –î?ß~‰°?±ÉÿK0mqŸŸÇo~‘` †;I°™R…Û_‚Ýþ$",uÉ_Øó¾èSo3Ûke®òtÝËD‚$¦|½üùü·v `?3Í&r$qÏb³S3ƒèÖyÔ"­fc¶íjõÌ?Ó.ž‰™‘`½¶õ÷^Bw4O F‚ ŸQB ÁpÐ tÕ)FLk/ìYl47]T‚äç‰#ÁH0`X@€a¸ƒ‹ì?K¬­*è^Ð]õ\^ë?óz]½]ZõzÝî»™éGOÇ!u´ æþ³Ç”ßÞH°Y¹Ç¢ÿ¹pÅéÃ$H0_·•`Dž 6rüIDŽ\!‚éêírV{“`Ø;15@‚á l$‹#ÁìaŒÃ]"ÁJ‘D¯Û³‘AÙr¶Žÿ^þûúV4TT´”¶½ë½Î‘ó‰¶Yô\Je—ê­Ã*í2R¿­6ií¿U¾ïj` võé‘(®VÊäCŒ´V½Ÿ’Œ †Ë0" w’`5™1±)'ºÿì:F%K­¼Þ:µÊΖsåv™vÙ[  Á®rÿÙÂç’#Á@‚ 6¡u„ËFèDÑŒHµì¿GÚ-+Á¢m8ëÚÙ.½ŸÝYeú® `î?x¼ûݲ¶÷-­G±—€‰lë9H0Œ ¶w9{Ö+RÇ'K°#Û…ƒA(@‚ÁýGÿ:X‚½¿/sœ- ¶‚8š%¼Îy$H0€#ÁH0 ¡ æþ¬!ÁJ‘XïÛ¶ökEQµÖkÒ¨T§èñ"ç{}_«ˆ0ìésï#h?ÜU‚Í)ÑéÙ<[½ àKï™!š2‰ë£‰ñ{ÄÔêí’} A¤îãà  ÁÜð8 –VÑH°ÑcöcÆ1{^'ÁH0lßc´®.ogDNQΘµ6Z‡Ûe…ºø®ÆƒÐI­÷GŽi nî>I‚áÂ,ù•‰ŠÊÈ´½ä\O4ÛH}A‚‘`Àµ#Á°¯TiEQ‡Û…áïï{ý÷¬c)Á Æ ÒÝ'I0\<,›ìÈè¯Þzõì-#Á@‚î(-}W#:-Es½oÛÚ¯¶^’Lµc”޳õzíØ‘sfî>I‚áÁ,›7+›KŒ#Á@‚$@‚a­AhThÕ„U+¢ª%œzêÐ[ $ †›%Æï>QÉV*£ö´ÅÖ±#‰ñ[S?[ ö[ÉóA‚ù""Á@‚$H°íH®LÔVKõDoe¢Ï"õ$Á@‚ÝW‚­ÐŸ£ÿ)p—~´»Á $H0$ŽŠë‘I=QY-q5« $Ø}$Øêý7z_»›0›:RH0` Á`¸›ë© ì~¬š‰6íÍ+˜Þ½µ^«_´ ZÿIÉ¿‘l½9·#x?æF‚$H0`H0ì?2(jMs¬ ¤²ÇÙ3@ †ã%ØÌ©Ï#y[õ+ý½GEEXë<{ÿS ¹Ö—»‘  Á ‹B !`xŠë®8C‚eËêR‘—DΑ æ ƒn"Á¾~œ)*Î|ÿH% †ûL‡Ì=Ë%¶¿‚Íc6*Á¦'ÆH0` ÁæÈ£„ÎìrΔx$ ‰© O“`£OŸ‘`½çK‚$H0\L‚½ÿ]AÙõÒë%Á´µ}«ž[õªÕ5zìŒÄkgé¼[ûŸA $@‚IŒŸÍÁ•É6K‚ŠªL"úVrýžˆ·L[JŒ $0Q‚mÉ ­õÖßQ –]ˆ¨¨È*;#õ¢\™cŸ= $@‚Ýïþ³E6*êˆÏÈHn­=ÊÉæ0;³‘` Á@‚ƒ‘`µˆ­£$X$¢+ù5{=#ÂŽ,$H0€CïýÇ=«oêæ-$Ø¿fð¯¿ß·ÏDH0 ¸Šk ž½"Á2S I0` Á î?Oí_ݬ&¨f ±WÉ`$p5 Ö’M³ä F‚‘`0H0¸ÿ`' öûüвøÚÚ¯$·ÞåÚ‘e Á@‚{I°ži‡‘§+Fãg¦Cf¤X&~+A}ö5‰ñI0„$ܰl$XKˆEö‹ ÁH0à, v†@ðû$ Bó‰gæºÙûý‘§Èí•ÇgÕüB$H0ÜF‚µ"ÉH0`$@‚‘` Á`:’˜9zÜ=$ÜÈ{fÖçîÒë.Ã}’ÃC"ÁjÛI0`$@‚$ž5ýý?ËË‘Q[ûE¤XÏqJõ)íyoíx­(°Ú1÷<·L=jûîÝæ$H0` Á@‚$@‚aéAhIŠDÖ³‘aÑãÔÖ3õ©ÏÈûgžÛŒ¶Þ»ÍI0`8]‚µ¦DÖD—§C‚#Á Á@‚E£‡Ž”`=b.r.+H°l]2×iï6'ÁpW ‰•ooG ` Á@‚ Ápd$XT¤¬"ÁzÎå*¬÷:‘`$ 6/ÿÉE‚ ` †J°ˆ#ÁH0,’‡.’Û/›;/ÓgZùõj÷Õè±H0 $Ø”›,°ßòÍ  Áp…ÄøÑè¯ì ¯7I{MèdêÑ3¥òès+åçŠ^§lFö÷tHì-Á2ùñJòk–À åÙ¹ I0 $ØàMÖuÃ:ýç'?>E˜A$@‚a¥A豫î"õ>âÜL»"Áž ËäÞ¨«ˆ$ß3ú“H0ý—`~p-*ÁVn_u8ª.¯? ¯ÔN™z=Q¦Ü!3rÏé8WÜEë¼÷¹`$ 6}F$ÁH0`$ §Þ%%Xíøg‰“#%ØÑÒèì6%Á€k> ÁH0Œ âõè?$X"²é}ÛÖ~¯*rŒ­÷”öËÊšR9¥siIŸÌ¶Z»”ê™=VOýjmPÚ¿÷Z` Á ëN‡ÌæÙjE½ÎÎi8£N$˜Á# âI°k‘Ù¾Cn«Í²Ü³H°³$XI¶DÖ{_+ɯ™¬µ‘`™(©ˆ@,¶Í3uë-‡s/ `ë?¸ìÉŸEŒƒA< –Q­m{ ¸lyO¾§ùÁuž‹D%µ^kcT¼´"Õ"õ-7ûï½…Ô ñE‚¹—€H0÷ŸÕ>w–KK°‘c“sñ$Ø5$XiÛ–Ìʾ?"Å^ߟ)CÿÁ‘`=r'zŒÙ¬§½Â'sî$ ƒP€ƒû)Áˆ0Œ;:déïÖzt¿¨èŠF‰µ„šþ¬ï¸#²‡#Á` `pÿÁR¬¹1!Ðf¢>`O‚­%Á¶¢¯2käýQéõ”ûˆ\ç'Æ_{O‡ŒF€EÎ¥7 þh~´Úñ{^­îã“`0H0÷ÜX‚õDhDöN•"Á@‚]7¬g:ãèûGŽ©ÿ ”8u‹™QQ³eDOýG£¹V2~’`¸ç tF^›=ĵ§µÃà †"ÁZ½ÜÞ(Œ#ÁΕ`{J¬Èû³‘^$˜\{N‡|räÏjõyêµ ÁpçAhM.Í:æŠl…A;q@‚=ñ?9qe>ö‘`3ȽQ$$ F‚/ÁZ‚«51’´>’D?3eû‰ùýàÚ:$ƒ;ä$X)šë}ÛÖ~µõRäTí¥ãl½^;vëøÑm‘h·R}jíú^f¶N ÁD‚aý§_’`0ˆ'Á..×@‚‘` †{ B£B«%”jƒÚVDUOfÕä}­úDÄÞȹ€#Áð8 ֊Ј&ËLcŠ&Ú‡A< vÝëªOë?$@‚{—[™¨­lTV$-}­×H9É7Rÿhû#ÁpÛœ`€A< ýGÎÙ\ÀH$XON°žH¬èÔ¿=£¿zr‚Ít=íŒ Äë?Ðîó#éûO~|æ?Û @Œ_ìÇgr/ÁU$بœ"Á@‚‘` ÁLÿþsåIß~‰°?¤ùì;ßÜK0š?’¸¾5ͱ–ì>{üžeµô½åd듵÷#Á@‚ñúô:¡¦¹žv Á@‚ƒA< `€ M‡l=Ý  $ â/$Áɪ0H0÷`€H0àÀþC‚ BÌý$˜AZÕ>$¢÷ÛJë#£}úW`¥H¡÷mïûÕÊ*E½¿«ŒÖqjå—öÝzo¤ŽÙzqÎ5U«ÏV¶Êiµwë|2õÛú¼E® `¡Ü_$î 6#ªräý>·ú\€«I°’¼ˆ¬GÄFKŽ´$HKŽD¥WM茶ÁÙçÜž­²3ç­Û¬ëã7pƒÄøµi\¥œDµíѼ]‘úl ‚V‚ýVò|Äï•Sot1y  ð ò©Eçì%„z¤\F6E¢µV9çÙ´Gz?ƒ³Êô› ¸ˆ3†A< ý€Õ"Áz“Ïšw†ë©û™ç<#Ÿ àð颠`¿ïtâÌC²Q™ú.üà`kJ°ÞrH0 À‘`€AüýGÿ ÿ¡äò™¤ïÙ$ñ£¬§î‘øGŸs6 ~¶#uíMÜ©»Äø ÄïÔDe‚\]‚EÉHŠ«•e˜Ó¾g×Åo2€H0@ÿˆ G ÝYÐ`sÚuÅÏŽßd `€þ Á€GÈ9¿É Áý@‚$ Á @‚$ ñ$@‚H0€p„‹ì?K |gÆñözŠßÑOý[áw’`£õÙó<Î:$@‚n%Á¾ÿäǧøü^žÆ/öã³/¸'7Š+É“×í½‘Oïål§vü÷í¥:níÛC­óŠF<­x™sê­K«ü'J°ìg欨=`~}û%ÂþÌgøæžÜL‚mI–ÙOYIÓ6ÑõÙç‘‚gŸGæœ"­gý ¬$[ÛŽ”)D $Xçú,͉&ÛûÜ®p½ç4«Ì+K°Öµi]ëÈûJí^ºQQ9»ÿ‚ì€rjT­)zgH°•΃ëë?£í•Z-±–‰ ÌF‚ìBltzå*l¯ó ÁæK°H”_ËägÉíF.‘`@‚uÐ{K+q|&2&’˜>;u1•µòyDs•E“æ?)1þˆœ<[‚í!±A‚À’,ʬdÛ{—ƒø«J°Y¢™Ó|! Á#ÁƒøíþyhAvÚáH´e&ÏÓ!õ ÁƒøTÿѧ ÿ Üv:¤è)è?@‚‰ô Á ЀƒA< `@‚Á žH0$˜A< âI0è?¾`ÉAüëï^÷-­§gÀwÄ ò¬² âI0€€$XôµˆHÛCd½Šµ3›Ýñ>ÐàâìK0E¢¿Z’+zœšØÊ£¶}«¼ZÙ¥ú‘ñ=,59SÔÎ8ÞŸ÷ž¶í{+œÇžQ©$lR$XïtÈžã´Žß;¥²T—u ¶’`¹£­Ïžç±BT* €»+E{DÑ;h%ÁH°£#ÁJòäu{oäS©_E#«j‘—™ÊÚ±³ùö"÷³Ïãì¨T vÁH°ì ˜#Á®(Áf³£RI0$ F‚‘`ËF‚)Á¢ÑL‘h²½Ïí çqö½ˆ@‚]D‚Õ³‘©N£O¥”Ÿ{’ëÉÁ×êgH°•΃€Å%ؽïÇ F‚­)ÁF§W®"Áö: . `çJ°",’8¾}Iâ>23•µòy¬•J‚ ÁH0`»I°(3Úæˆr ÿ Äë?ЀH0@ÿ€E%°$@‚Àt öý'?>ÅÃçà 8“_ìÇçç’H0˜)Á¾ýa‹ðùyüF‚$L“`Æ$`:1`ÀÝ%˜éÄ0H0àöÌtb˜N $0Ÿÿ»õ6-$—IEND®B`‚debpartial-mirror/doc/HACKING0000644000000000000000000000526711647020704013132 0ustar This small file, documents how is the current class layout of debpartial-mirror tool. Package - store information about a package SourcePackage - store the extra information needed to deal with source packages using a list of dictionaries PackageList - just what it sounds like, a listing of packages from a single source (Functionality: provide a list of packages and where to get them from; tell what packages depend on other packages; perform filtering on the list and provide a subset of packages) Dists - handle creation of dists dir into the partial-mirror. | +---------> RemoteDists - download index files (Packages.gz, | Sources.gz, Release, sum files,...) from | remote repository | +---------> LocalDists - move index files (Packages.gz, Sources.gz, Release, sum files,...) from local repository Pool - handle creation of "pool"s dir into the partial-mirror. | +---------> RemotePool - download packages from remote repository | +---------> LocalPool - move packages from remote repository (Functionality: grab packages into a MirrorBackend) Backend - a locally-stored APT repository that debpartial-mirror | generates | +-----> MirrorBackend - gets packages from a PackageSource, with | optional filtering | +-----> MergeBackend - gets packages from MirrorBackends, with optional additional filtering (Functionality: figure out which packages need updating; pull updates from source) Config(ConfigParser.ConfigParser) - the debpartial-mirror configuration | database, read from a text file | +-------> uses several ConfigSection objects (see below) (Functionality: read the config file, verify that its format is ok, and store it in memory. Allow access to config options.) ConfigSection - a section of the config file | +-------> ConfigGlobal - the GLOBAL section of the config file | +-------> ConfigBackend - a backend section | +------> ConfigBackendMirror - a mirroring backend (pulls | package files from a | remote repository) | +------> ConfigBackendMerge - a merging backend (combines several mirroring backends into one repository) (Functionality: read one section of the config file and verify that its members are correctly formatted, then give it back to the Config object.) ---------------------------------------------------------------------- $Id$ debpartial-mirror/tests/0000755000000000000000000000000011647020704012526 5ustar debpartial-mirror/tests/TestDownload.py0000644000000000000000000001261311647020704015512 0ustar # debpartial-mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador , Nat Budin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import unittest import threading import logging import SimpleHTTPServer import BaseHTTPServer import os import sys from debpartial_mirror import Download from TestBase import TestBase # Let us make our server quiet! ;-) class MySimpleHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): def log_message(self, format, *args): pass # don't output # Our builtin simple HTTP server class ServerThread(threading.Thread): def __init__(self, port, HandlerClass = MySimpleHTTPRequestHandler, ServerClass = BaseHTTPServer.HTTPServer, protocol="HTTP/1.1"): server_address = ('', port) HandlerClass.protocol_version = protocol self.httpd = ServerClass (server_address, HandlerClass) threading.Thread.__init__(self) def run(self): self.httpd.serve_forever() class DownloadTests(TestBase): size_list = (1000, 5000, 10000) running_servers = [] port = 80 def setUp (self): os.environ["http_proxy"] = "" self.__createFiles() def tearDown(self): self.__removeFiles() def __start_http(self): while True: try: httpServer = ServerThread(self.port) httpServer.setDaemon(True) httpServer.start() break except: self.port += 1 pass print "running on port ",self.port self.running_servers.append(self.port) def __get_http_ports(self): return self.running_servers def __createFile (self, name, size): fname = self.aux_file(name) cmd = "dd if=/dev/zero of=%s bs=1024 count=%s 2>/dev/null" % (fname, size) self.failUnlessEqual(os.system(cmd), 0, 'Failed to create input test file.') def __createFiles (self): for size in self.size_list: fname = "pkg-example_1.%d_all.deb" % size self.__createFile(fname, size) def __removeFile (self, name): if os.path.exists(self.aux_file(name)): os.remove (self.aux_file(name)) if os.path.exists(name): os.remove (name) def __removeFiles (self): for size in self.size_list: fname = "pkg-example_1.%d_all.deb" % size self.__removeFile (fname) def test_1download_without_server(self): """Download: Try to grab a file while HTTPserver is down.""" d = Download.Download(info="quiet", name="Test1") size = self.size_list[0] filename = "pkg-example_1.%d_all.deb" % size self.__createFile (filename, size) d.get('http://invalid/%s' % filename, filename) d.wait_mine() self.failIf(os.path.exists(filename), "Created (empty) downloaded file even if HTTP server was down.") del d self.__removeFile(filename) def test_2download_one(self): """Download: grab a file.""" # Initialize a HTTP server self.__start_http() d = Download.Download(info="quiet", name="Test2") filename = "pkg-example_1.%d_all.deb" % self.size_list[0] self.__createFile (filename, self.size_list[0]) d.get(self.makeUrl(0, filename), filename) d.wait_mine() self.failUnlessEqual(os.path.getsize(filename), os.path.getsize(self.aux_file(filename)), "Downloaded file of wrong size.") del d self.__removeFiles() def test_3download_a_non_existing_file(self): """Download: grab a non existing file.""" # Initialize a HTTP server self.__start_http() d = Download.Download(info="quiet", name="Test3") filename = "pkg-example-invalid.%d_all.deb" % self.size_list[0] #self.__createFile (filename, self.size_list[0]) d.get(self.makeUrl(0, filename), filename) d.wait_mine() self.failIf(os.path.exists(filename), "Downloaded file even if was not exsistent on remote server.") del d def test_4dowload_more_file(self): """Download: multiple file download""" # Initialize a HTTP server self.__start_http() self.__start_http() self.__start_http() d = Download.Download(info="quiet",name= "Test 4") self.__createFiles() for f in range (0,len(self.size_list)): localname = "pkg-example_1.%d_all.deb" % self.size_list[f] d.get(self.makeUrl(f, localname), localname) d.wait_mine() for f in range (0,len(self.size_list)): localname = "pkg-example_1.%d_all.deb" % self.size_list[f] filename = self.aux_file(localname) self.failUnlessEqual(os.path.getsize(filename), os.path.getsize(localname), "Downloaded file has wrong size.") del d self.__removeFiles() def makeUrl(self, portIndex, filename): return "http://localhost:%d/%s/%s" % ( self.running_servers[portIndex], self.aux_rel_path(), filename ) def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(DownloadTests, 'test')) return suite if __name__ == '__main__': log = logging.getLogger() log_handler = logging.FileHandler(sys.argv[0][:-3] + '.log') log.setLevel(logging.DEBUG) log.addHandler(log_handler) unittest.main(defaultTest='suite') debpartial-mirror/tests/tests.py0000644000000000000000000000033311647020704014241 0ustar #!/usr/bin/python import unittest import sys sys.path.append('lib') import __init__ if not unittest.TextTestRunner(verbosity=2).\ run(__init__.suite()).wasSuccessful(): sys.exit(1) debpartial-mirror/tests/TestEndToEnd.py0000644000000000000000000006762711647020704015422 0ustar import os import unittest import mock from TestBase import TestBase from RepositorySimulator import PooledRepository, NonPooledRepository from debpartial_mirror import Backend from debpartial_mirror import Config from debpartial_mirror import Controller from debpartial_mirror import Dists from debpartial_mirror import Download from debpartial_mirror import Failures class EndToEndTests(TestBase): def setUp(self): TestBase.setUp(self) self.mockCommandExecutor = mock.Mock() Dists.Dists._commandExecutor = self.mockCommandExecutor.execute def test01a(self): """ Mirror a pooled repository - basic """ sourceRepository = PooledRepository() self.addPackages(sourceRepository, ["package1", "package2"], distribution="etch", component="main", architecture="i386") self.addInstallerPackages(sourceRepository, ["udeb1", "udeb2"], distribution="etch", component="main", architecture="i386") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( components="main main/debian-installer", ) configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir,multi=True) self.checkPackages( ["package1", "package2"], mirroredRepository, distribution="etch", component="main", architecture="i386", ) self.checkInstallerPackages( ["udeb1", "udeb2"], mirroredRepository, distribution="etch", component="main", architecture="i386" ) def test01b(self): """ Mirror a pooled repository - partial archs""" sourceRepository = PooledRepository() self.addPackages(sourceRepository, ["package1"], distribution="etch", component="main", architecture="i386") self.addPackages(sourceRepository, ["package2"], distribution="etch", component="main", architecture="alpha") self.addPackages(sourceRepository, ["package3"], distribution="etch", component="main", architecture="arm") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( architectures = "i386 arm", ) configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir,multi=True) self.checkPackages(["package1"], mirroredRepository, distribution="etch", component="main", architecture="i386") self.checkPackages([], mirroredRepository, distribution="etch", component="main", architecture="alpha") self.checkPackages(["package3"], mirroredRepository, distribution="etch", component="main", architecture="arm") def test01c(self): """ Mirror a pooled repository - partial components""" sourceRepository = PooledRepository() self.addPackages(sourceRepository, ["package1"], distribution="etch", component="main", architecture="i386") self.addPackages(sourceRepository, ["package2"], distribution="etch", component="contrib", architecture="i386") self.addPackages(sourceRepository, ["package3"], distribution="etch", component="non-free", architecture="i386") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( components = "main contrib", ) configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir,multi=True) self.checkPackages(["package1"], mirroredRepository, distribution="etch", component="main", architecture="i386") self.checkPackages(["package2"], mirroredRepository, distribution="etch", component="contrib", architecture="i386") self.checkPackages([], mirroredRepository, distribution="etch", component="non-free", architecture="i386") def test01d(self): """ Mirror a pooled repository - partial distributions""" sourceRepository = PooledRepository() self.addPackages(sourceRepository, ["package1"], distribution="etch", component="main", architecture="i386") self.addPackages(sourceRepository, ["package2"], distribution="lenny", component="main", architecture="i386") self.addPackages(sourceRepository, ["package3"], distribution="sid", component="main", architecture="i386") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( distributions = "etch lenny", ) configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir,multi=True) self.checkPackages(["package1"], mirroredRepository, distribution="etch", component="main", architecture="i386") self.checkPackages(["package2"], mirroredRepository, distribution="lenny", component="main", architecture="i386") self.checkPackages([], mirroredRepository, distribution="sid", component="main", architecture="i386") def test01e(self): """ Mirror a pooled repository - same package in multiple archs""" sourceRepository = PooledRepository() self.addPackages(sourceRepository, ["package1"], distribution="etch", component="main", architecture="i386") self.addPackages(sourceRepository, ["package1", "package3"], distribution="etch", component="main", architecture="arm") self.addPackages(sourceRepository, ["package2"], distribution="etch", component="main", architecture="alpha") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( architectures = "i386 arm", ) configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir,multi=True) self.checkPackages(["package1"], mirroredRepository, distribution="etch", component="main", architecture="i386") self.checkPackages([], mirroredRepository, distribution="etch", component="main", architecture="alpha") self.checkPackages(["package1", "package3"], mirroredRepository, distribution="etch", component="main", architecture="arm") def test01f(self): """ Mirror a pooled repository : keep latest version per architecture """ sourceRepository = PooledRepository() self.addPackages(sourceRepository, ["package1_1.0", "package1_1.1"], distribution="etch", component="main", architecture="i386") self.addPackages(sourceRepository, ["package1_1.2"], distribution="etch", component="main", architecture="arm") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( architectures = "i386 arm", ) configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir,multi=True) #self.failUnlessEqual(("1.0", "1.1"), sourceRepository.getAvailableVersions("package1", distribution="etch", component="main", architecture="i386")) self.failUnlessEqual(("1.1", ), mirroredRepository.getAvailableVersions("package1", distribution="etch", component="main", architecture="i386")) self.failUnlessEqual(("1.2", ), mirroredRepository.getAvailableVersions("package1", distribution="etch", component="main", architecture="arm")) def test02a(self): """ Mirror a pooled repository - filters """ sourceRepository = PooledRepository() self.addPackages(sourceRepository, ["package1", "package2", "stuff1", "stuff2"], distribution="etch", component="main", architecture="i386") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s filter = name:.*1 """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["package1", "stuff1"], mirroredRepository, distribution="etch", component="main", architecture="i386" ) def test02b(self): """ Mirror a pooled repository - filters with dependencies""" sourceRepository = PooledRepository() packageList = self.addPackages(sourceRepository, ["skipped", "depends1", "suggests1", "recommends1", "requiredButExcluded"], distribution="etch", component="main", architecture="i386" ) packageList.addPackage("required1", Depends="depends1", Suggests="suggests1", Recommends="recommends1") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s filter = name:required.* exclude-name:.*Excluded """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( #FIXME: not actually implemented... #["required1", "depends1", "recommends1"], ["required1", "depends1"], mirroredRepository, distribution="etch", component="main", architecture="i386" ) def test02c(self): """ Mirror a pooled repository - filters with unmet dependencies (non standalone)""" sourceRepository = PooledRepository() packageList = self.addPackages(sourceRepository, ["skipped", "depends1", "depends3"], distribution="etch") packageList.addPackage("required1", Depends="depends1, depends2, depends3") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( get_recommends = "true", ) configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s filter = name:required.* """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["required1", "depends1", "depends3"], mirroredRepository, distribution="etch", component="main", architecture="i386" ) def test02d(self): """ Mirror a pooled repository - filters with unmet dependencies (standalone)""" sourceRepository = PooledRepository() packageList = self.addPackages(sourceRepository, ["skipped", "depends1", "depends3"], distribution="etch" ) packageList.addPackage("required1", Depends="depends1, depends2, depends3, depends4") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s filter = name:required.* standalone = true """% sourceDir) try : self.doAll(configuration) self.fail("Exception not raised") except Failures.DependencyResolutionFailure, ex: print ex.format() self.failUnlessEqual("required1 needs [depends2,depends4]", ex.format()) def test02e(self): """ Mirror a pooled repository - filters with architecture specific dependencies""" sourceRepository = PooledRepository() packageList1 = self.addPackages(sourceRepository, ["depends1",], distribution="etch", component="main", architecture="i386" ) packageList2 = self.addPackages(sourceRepository, ["depends2",], distribution="etch", component="main", architecture="arm" ) packageList1.addPackage("required1", Depends="depends1") packageList2.addPackage("required2", Depends="depends2") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration(architectures="i386 arm") configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s filter = name:required.* exclude-name:.*Excluded """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir, multi=True) self.checkPackages( ["required1", "depends1"], mirroredRepository, distribution="etch", component="main", architecture="i386" ) self.checkPackages( ["required2", "depends2"], mirroredRepository, distribution="etch", component="main", architecture="arm" ) def test03a(self): """ Mirror a source repositroy - basic """ sourceRepository = PooledRepository() # FIXME: pure source repositories not supported even if get_packages=false self.addPackages(sourceRepository, ["package1", "package2"], distribution="etch", component="main", architecture="i386") packageList = sourceRepository.createSourcePackageList(distribution="etch", component="main") packageList.addPackage("srcPackage1") packageList.addPackage("srcPackage2") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( get_packages = "false", get_sources = "true", ) configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) return # FIXME: source packages not currently fetched!! self.checkSourcePackages( ["srcPackage1", "srcPackage2"], mirroredRepository, distribution="etch", component="main", ) def test04a(self): """ Merge - ensure all package fields copied """ sourceRepository = PooledRepository() packageList = sourceRepository.createPackageList(distribution="etch", component="main", architecture="i386") fields = { "Essential":"yes", "Priority":"required", "Section":"shells", "Installed-Size":"1999", "Maintainer":"a.n other", "Replaces":"old stuff", "Depends":"depends1", "Pre-Depends":"predepends1", "Suggests":"suggests1", "Recommends":"recommends1", "Size":"123456", "MD5sum":"md5 sum", "SHA1":"sha1 sum", "SHA256":"sha256 sum", "Description":"Short description\n line1\ line2", "Tag":"tag1", } packageList.addPackage("package1", **fields) packageList.addPackage("depends1") packageList.addPackage("suggests1") packageList.addPackage("recommends1") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [src1] server = file://%s [myRepo] backends = src1 """% sourceDir) self.doAll(configuration) mergedRepository = self.readMirror(PooledRepository, mirrorDir) package = mergedRepository.getPackage("package1", distribution="myRepo", component="main", architecture="i386") for key, expectedValue in fields.iteritems(): self.failUnlessEqual(expectedValue, package[key]) def test04b(self): """ Merge two pooled repositories - basic with default signature""" sourceRepository1 = PooledRepository() self.addPackages(sourceRepository1, ["A", "C"], distribution="etch", component="main", architecture="i386") sourceDir1 = self.writeRepository(sourceRepository1) sourceRepository2 = PooledRepository() self.addPackages(sourceRepository2, ["B", "D"], distribution="etch", component="main", architecture="i386") sourceDir2 = self.writeRepository(sourceRepository2) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [src1] server = file://%s [src2] server = file://%s [myRepo] backends = src1 src2 signature_key = default """% (sourceDir1, sourceDir2)) self.mockCommandExecutor.execute.return_value = 0 self.doAll(configuration) self.mockCommandExecutor.execute.assert_called_with( "gpg -abs -o - %s/myRepo/dists/myRepo/Release > %s/myRepo/dists/myRepo/Release.gpg" % (mirrorDir, mirrorDir) ) mergedRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["A", "B", "C", "D"], mergedRepository, distribution="myRepo", component="main", architecture="i386" ) def test04c(self): """ Merge two pooled repositories - debian installer and specific signature""" sourceRepository1 = PooledRepository() self.addInstallerPackages(sourceRepository1, ["udebA", "udebC"], distribution="etch", component="main", architecture="i386") sourceDir1 = self.writeRepository(sourceRepository1) sourceRepository2 = PooledRepository() self.addInstallerPackages(sourceRepository2, ["udebB", "udebD"], distribution="etch", component="main", architecture="i386") sourceDir2 = self.writeRepository(sourceRepository2) globalConfiguration, mirrorDir = self.createGlobalConfiguration( components = "main/debian-installer", ) configuration = self.loadConfiguration(globalConfiguration + """ [src1] server = file://%s [src2] server = file://%s [myRepo] backends = src1 src2 signature_key = DEADBEEF """% (sourceDir1, sourceDir2)) self.mockCommandExecutor.execute.return_value = 0 self.doAll(configuration) self.mockCommandExecutor.execute.assert_called_with( "gpg -abs -u DEADBEEF -o - %s/myRepo/dists/myRepo/Release > %s/myRepo/dists/myRepo/Release.gpg" % (mirrorDir, mirrorDir) ) mergedRepository = self.readMirror(PooledRepository, mirrorDir) self.checkInstallerPackages( ["udebA", "udebB", "udebC", "udebD"], mergedRepository, distribution="myRepo", component="main", architecture="i386" ) def test04d(self): """ Merge from a dual part components (etch/updates) """ sourceRepository = PooledRepository() self.addPackages(sourceRepository, ["A", "B"], distribution="etch", component="updates/main", architecture="i386") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration( components = "updates/main", ) configuration = self.loadConfiguration(globalConfiguration + """ [src1] server = file://%s [myRepo] backends = src1 """% sourceDir) self.doAll(configuration) mergedRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["A", "B"], mergedRepository, distribution="myRepo", component="updates/main", architecture="i386" ) def test05a(self): """ Merge with filter """ sourceRepository1 = PooledRepository() self.addPackages(sourceRepository1, ["inc1", "exc1", "inc2", "exc2"], distribution="etch") sourceDir1 = self.writeRepository(sourceRepository1) sourceRepository2 = PooledRepository() packageList = self.addPackages(sourceRepository2, ["got1", "exc3", "got2", "exc4"], distribution="etch") packageList.addPackage("depends1") packageList.addPackage("suggests1") packageList.addPackage("recommends1") packageList.addPackage("gotWithDeps", Depends="depends1", Suggests="suggests1", Recommends="recommends1") sourceDir2 = self.writeRepository(sourceRepository2) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [src1] server = file://%s [src2] server = file://%s [myRepo] backends = src1 src2 filter_src1 = name:inc filter_src2 = name:got """% (sourceDir1, sourceDir2)) self.doAll(configuration) mergedRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["depends1", "got1", "got2", "gotWithDeps", "inc1", "inc2"], mergedRepository, distribution="myRepo", component="main", architecture="i386" ) def test06a(self): """ Mirror a non pooled repository - basic """ sourceRepository = NonPooledRepository() self.addPackages(sourceRepository, ["package1", "package2"], distribution="etch", component="main", architecture="i386") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [myRepo] server = file://%s """% sourceDir) self.doAll(configuration) mirroredRepository = self.readMirror(NonPooledRepository, mirrorDir) self.checkPackages(["package1", "package2"], mirroredRepository, distribution="etch", component="main", architecture="i386") def test06b(self): """ Merge two non pooled repositories """ sourceRepository1 = NonPooledRepository() self.addPackages(sourceRepository1, ["A", "C"], distribution="etch", component="main", architecture="i386") sourceDir1 = self.writeRepository(sourceRepository1) sourceRepository2 = NonPooledRepository() self.addPackages(sourceRepository2, ["B", "D"], distribution="etch", component="main", architecture="i386") sourceDir2 = self.writeRepository(sourceRepository2) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [src1] server = file://%s [src2] server = file://%s [myRepo] backends = src1 src2 """% (sourceDir1, sourceDir2)) self.doAll(configuration) mergedRepository = self.readMirror(NonPooledRepository, mirrorDir) self.checkPackages( ["A", "B", "C", "D"], mergedRepository, distribution="myRepo", component="main", architecture="i386" ) def test07a(self): """ Mirror update on filter change """ sourceRepository = PooledRepository() packageList = self.addPackages(sourceRepository, ["skipped1", "skipped2", "depends1"], distribution="etch") packageList.addPackage("required1", Depends="depends1") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration() baseConfigFile = globalConfiguration +""" [myRepo] server = file://%s """% sourceDir self.doAll(self.loadConfiguration(baseConfigFile)) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["required1", "depends1", "skipped1", "skipped2"], mirroredRepository, distribution="etch", component="main", architecture="i386" ) print 80*"=" Download.Curl.buggy_curl = True # seems to have problems on my machine even though OK... self.doAll(self.loadConfiguration(baseConfigFile + "filter = name:required.*")) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["required1", "depends1"], mirroredRepository, distribution="etch", component="main", architecture="i386" ) print 80*"=" self.doAll(self.loadConfiguration(baseConfigFile)) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["required1", "depends1", "skipped1", "skipped2"], mirroredRepository, distribution="etch", component="main", architecture="i386" ) def test07b(self): """ Merge mirror update on filter change """ sourceRepository = PooledRepository() packageList = self.addPackages(sourceRepository, ["skipped1", "skipped2", "depends1"], distribution="etch") packageList.addPackage("required1", Depends="depends1") sourceDir = self.writeRepository(sourceRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration() baseConfigFile = globalConfiguration +""" [myRepo] backends = source [source] server = file://%s """% sourceDir self.doAll(self.loadConfiguration(baseConfigFile)) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["required1", "depends1", "skipped1", "skipped2"], mirroredRepository, distribution="myRepo", component="main", architecture="i386" ) print 80*"=" Download.Curl.buggy_curl = True # seems to have problems on my machine even though OK... self.doAll(self.loadConfiguration(baseConfigFile + "filter = name:required.*")) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["required1", "depends1"], mirroredRepository, distribution="myRepo", component="main", architecture="i386" ) print 80*"=" self.doAll(self.loadConfiguration(baseConfigFile)) mirroredRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["required1", "depends1", "skipped1", "skipped2"], mirroredRepository, distribution="myRepo", component="main", architecture="i386" ) def test08a(self): """ Dependency resolution from another repository (resolve_deps_using) """ sourceRepository = PooledRepository(missingReleaseFields=("Version",)) packageList = self.addPackages(sourceRepository, ["pack1", "pack2",], distribution="etch") packageList.addPackage("pack3", Depends="dep1, dep2") depRepository = PooledRepository() self.addPackages(depRepository, ["dep1", "dep2", "other1"], distribution="etch") sourceDir = self.writeRepository(sourceRepository) depDir = self.writeRepository(depRepository) globalConfiguration, mirrorDir = self.createGlobalConfiguration() configuration = self.loadConfiguration(globalConfiguration + """ [dep] server = file://%s [myRepo] server = file://%s resolve_deps_using = dep signature_key = default """% (depDir, sourceDir)) self.mockCommandExecutor.execute.return_value = 0 self.doAll(configuration) self.mockCommandExecutor.execute.assert_called_with( "gpg -abs -o - %s/myRepo/dists/etch/Release > %s/myRepo/dists/etch/Release.gpg" % (mirrorDir, mirrorDir) ) myRepository = self.readMirror(PooledRepository, mirrorDir) self.checkPackages( ["pack1", "pack2", "pack3", "dep1", "dep2"], myRepository, distribution="etch", component="main", architecture="i386" ) for field, value in sourceRepository.getReleaseFields().iteritems(): if not field in ("Date",): self.failUnlessEqual(value, myRepository.getReleaseFields()[field]) #TODO: trivial repositories def createGlobalConfiguration(self, **kw): configMap = { "architectures" :"i386", "components" : "main", "distributions" : "etch", "get_suggests" : "false", "get_recommends" : "false", "get_provides" : "false", "get_sources" : "false", "get_packages" : "true", } configMap.update(kw) mirrorDir = self.createTempDirectory() configMap["mirror_dir"] = mirrorDir configLines = ["[GLOBAL]"] for key, value in configMap.iteritems(): configLines.append("%s = %s" % (key, value)) configLines.append("") return "\n".join(configLines), mirrorDir def readMirror(self, class_, mirrorDir, name="myRepo", multi=False): mirroredRepository = class_(multi=multi) mirroredRepository.readFromFilesystem(os.path.join(mirrorDir, name)) return mirroredRepository def addPackages(self, repository, packageNames, distribution, component="main", architecture="i386"): return self._addPackages(repository.createPackageList(distribution, component, architecture), packageNames) def addInstallerPackages(self, repository, packageNames, distribution, component="main", architecture="i386"): return self._addPackages(repository.createInstallerPackageList(distribution, component, architecture), packageNames) def _addPackages(self, packageList, packageNames): for name in packageNames: packageList.addPackage(name) return packageList def writeRepository(self, repository): directory = os.path.abspath(self.createTempDirectory()) # always absolute for use in file:// urls repository.writeToFilesystem(directory) return directory def loadConfiguration(self, configString): configFile = self.createTempFile() f = open(configFile, "w") try: f.write(configString) finally: f.close() return Config.Config(configFile) def doAll(self, configuration): Backend.Backend.backends = [] #TODO: fix static Download.Download.d_fetchers_list = {} controller = Controller.Controller(configuration, []) controller.executeCommand("all") def checkPackages(self, expectedPackageNames, repository, distribution, component="main", architecture="i386"): self.failUnlessEqual( sorted(expectedPackageNames), sorted(repository.getAvailablePackageNames(distribution, component, architecture)), ) if not repository.isMulti(): # Cannot tell which files for which arch,component etc just by looking at filenames self.failUnlessEqual( sorted(expectedPackageNames), sorted(repository.getPackageNamesForPresentFiles()), ) def checkInstallerPackages(self, expectedPackageNames, repository, distribution, component="main", architecture="i386"): self.failUnlessEqual( sorted(expectedPackageNames), sorted(repository.getAvailableInstallerPackageNames(distribution, component, architecture)), ) def checkSourcePackages(self, expectedPackageNames, repository, distribution, component="main"): self.failUnlessEqual( sorted(expectedPackageNames), sorted(repository.getAvailableSourcePackageNames(distribution, component)), ) def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(EndToEndTests, 'test')) return suite if __name__ == '__main__': unittest.main() debpartial-mirror/tests/TestConfig.py0000644000000000000000000001030611647020704015145 0ustar # debpartial-mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador , Nat Budin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import unittest from TestBase import TestBase from debpartial_mirror import Config class ConfigurationFilesTests(TestBase): def loadConfig(self, conf_file, exception): """ Generic test code to handle configuration file load and exception handling for it. """ try: cnf = Config.Config(conf_file) return cnf except Config.InvalidOption, msg: if not exception or not isinstance(msg, exception): self.fail("""Wrongly intercept as Config.InvalidOption exception. %s was expected. section: %s option: %s""" % (type(exception), msg.section, msg.option)) except Config.InvalidSection, msg: if not exception or not isinstance(msg, exception): self.fail("""Wrongly intercept as Config.InvalidSection exception. %s was expected. section: %s""" % (type(exception), msg.section)) except Config.RequiredOptionMissing, msg: if not exception or not isinstance(msg, exception): self.fail("""Wrongly intercept as Config.RequiredOptionMissing exception. %s was expected. section: %s option: %s""" % (type(exception), msg.section, msg.option)) if not exception: self.fail("Fail to raise the need exception. %s was expected." % type(exception)) def testGoodConfigurationFile(self): """Config: valid configuration file""" self.loadConfig(self.aux_file('TestConfig-good.conf'), None) def testMissingRequiredOption(self): """Config: missing required option""" self.loadConfig(self.aux_file('TestConfig-missing-required.conf'), Config.RequiredOptionMissing) def testInvalidOption(self): """Config: invalid option""" self.loadConfig(self.aux_file('TestConfig-invalid-option.conf'), Config.InvalidOption) def testInvalidOptionValue(self): """Config: invalid option value""" self.loadConfig(self.aux_file('TestConfig-invalid-option-value.conf'), Config.InvalidOption) def testSectionType(self): """Config: invalid option due a section type""" self.loadConfig(self.aux_file('TestConfig-section-types.conf'), Config.InvalidOption) def testInvalidFilter(self): """Config: invalid filter""" self.loadConfig(self.aux_file('TestConfig-invalid-filter.conf'), Config.InvalidOption) def testInvalidSection(self): """Config: invalid section""" self.loadConfig(self.aux_file('TestConfig-invalid-section.conf'), Config.InvalidSection) def testTypeCast(self): """Config: type casting""" def castCompare(option, section, right): self.failUnless(cnf.get_option(option, section) == right, "We were expecting to receive '%s' but we received '%s'." % (right, cnf.get_option(option, section))) cnf = self.loadConfig(self.aux_file('TestConfig-typecast.conf'), None) # List conversion castCompare('architectures', 'GLOBAL', ['i386', 'powerpc']) castCompare('components', 'GLOBAL', ['main']) castCompare('distributions', 'GLOBAL', ['stable']) # Boolean conversion castCompare('get_suggests', 'GLOBAL', False) castCompare('get_recommends', 'GLOBAL', True) castCompare('get_provides', 'GLOBAL', True) castCompare('get_packages', 'GLOBAL', True) castCompare('get_sources', 'GLOBAL', False) def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(ConfigurationFilesTests, 'test')) return suite if __name__ == '__main__': import logging, sys log = logging.getLogger() log_handler = logging.FileHandler(sys.argv[0][:-3] + '.log') log.setLevel(logging.DEBUG) log.addHandler(log_handler) unittest.main(defaultTest='suite')debpartial-mirror/tests/RepositorySimulator.py0000644000000000000000000003256111647020704017166 0ustar import gzip import hashlib import os import re import StringIO import apt_pkg class AbstractRepository: def __init__(self, rawIndex=False, gzIndex=True, multi=False, releaseFields=None, missingReleaseFields=()): self._hasRawIndex = rawIndex self._hasGzIndex = gzIndex self._multi = multi self._releaseFields = { "Origin" : "Repository Simulator", "Label" : "Simulator Label", "Suite" : "Simulator Suite", "Version" : "Simulator Version", "Date" : "Simulator Date", "Description" : "Simulator Description", } for fieldname in missingReleaseFields: self._releaseFields.pop(fieldname, None) if releaseFields is not None: self._releaseFields.update(releaseFields) self._packageLists = {} self._installerPackageLists = {} self._sourcePackageLists = {} def getReleaseFields(self): return self._releaseFields def isMulti(self): return self._multi def createPackageList(self, distribution, component, architecture): packageList = _PackageList() self._packageLists[(distribution, component, architecture)] = packageList return packageList def createInstallerPackageList(self, distribution, component, architecture): packageList = _InstallerPackageList() self._installerPackageLists[(distribution, component, architecture)] = packageList return packageList def createSourcePackageList(self, distribution, component): packageList = _SourcePackageList() self._sourcePackageLists[(distribution, component)] = packageList return packageList def writeToFilesystem(self, baseDir): for filename, contents in self.implCalculateFiles().iteritems(): pathname = os.path.join(baseDir, filename) dirname = os.path.dirname(pathname) if not os.path.exists(dirname): os.makedirs(dirname) f = open(pathname, "w") try: f.write(contents) finally: f.close() def _addIndexFile(self, files, filename, contents): if self._hasRawIndex: files[filename] = contents if self._hasGzIndex: buf = StringIO.StringIO() compressor = gzip.GzipFile(mode="wb", fileobj=buf) compressor.write(contents) compressor.close() files[filename + ".gz"] = buf.getvalue() buf.close() return files def _addReleaseFile(self, files, path, distribution, component, architecture): fields = [] for key, value in ( ("Archive", distribution), ("Component", component), ("Architecture", architecture), ): fields.append("%s: %s" % (key, value)) files[os.path.join(path, "Release")] = "\n".join(fields) def implCalculateFiles(self): raise NotImplementedError() class AutomaticRepository(AbstractRepository): def readFromFilesystem(self, baseDir): self._baseDir = baseDir distDir, dists, _ = os.walk(os.path.join(baseDir, "dists")).next() def loadPackages(packageListFactory, key, indexFile): packageList = packageListFactory(*key) if os.path.exists(indexFile): for packageInfo in parseTagFile(indexFile): packageList.addPackage(packageInfo["Package"], **packageInfo) for dist in dists: releaseFile = os.path.join(distDir, dist, "Release") if not os.path.exists(releaseFile): print "%s not found" % releaseFile continue release = parseTagFile(releaseFile)[0] for field in self._releaseFields.keys(): self._releaseFields[field] = release.get(field, "") for component in release["Components"].split(" "): for architecture in release["Architectures"].split(" "): loadPackages( self.createPackageList, (dist, component, architecture), os.path.join(distDir, dist, component, "binary-%s" % architecture , "Packages") ) loadPackages( self.createInstallerPackageList, (dist, component, architecture), os.path.join(distDir, dist, component, "debian-installer", "binary-%s" % architecture , "Packages") ) loadPackages( self.createSourcePackageList, (dist, component), os.path.join(distDir, dist, component, "source", "Sources") ) def getPackage(self, name, distribution, component, architecture): packageList = self._packageLists.get((distribution, component, architecture)) return packageList.getPackage(name) def getPackageNamesForPresentFiles(self): """ Obtain package names corresponding to package files present (for cleanup tests) """ packageNames = [] packageRe = re.compile("^([^_]+)_(.*)\.(dsc|(diff|tar)\.gz|deb|udeb)") for dirpath, dirname, filenames in os.walk(self._baseDir): for filename in filenames: match = packageRe.match(filename) if match: packageNames.append(match.group(1)) return packageNames def getAvailablePackageNames(self, distribution, component, architecture): """ Obtain package names that are installable (in index files and package files) """ return self._getAvailablePackageNames( self._packageLists, (distribution, component, architecture), self._collectSingleFilePackageNames, ) def getAvailableInstallerPackageNames(self, distribution, component, architecture): return self._getAvailablePackageNames( self._installerPackageLists, (distribution, component, architecture), self._collectSingleFilePackageNames, ) def getAvailableVersions(self, packageName, distribution, component, architecture): def collectSingleFilePackageVersion(availableVersions, package): if self._packageFileExists(package): availableVersions.append(package["Version"]) versions = self._getAvailablePackageNames( self._packageLists, (distribution, component, architecture), collectSingleFilePackageVersion, ) versions.sort() return tuple(versions) def _collectSingleFilePackageNames(self, availableNames, package): if self._packageFileExists(package): availableNames.append(package["Package"]) def getAvailableSourcePackageNames(self, distribution, component): def collector(availableNames, package): for filename in package["filenames"]: if os.path.exists(os.path.join(self._baseDir, package["Directory"], filename)): availableNames.append(package["Package"]) return self._getAvailablePackageNames( self._sourcePackageLists, (distribution, component), collector, ) def _packageFileExists(self, package): filename = os.path.join(self._baseDir, package["Filename"]) exists = os.path.exists(filename) if not exists: print "*** %s not found" % filename return exists def _getAvailablePackageNames(self, packageLists, key, collector): packageList = packageLists.get(key) if packageList is None: return [] availableNames = [] for package in packageList.iterfields(): collector(availableNames, package) return availableNames def _collectPackageFiles(self, files, filenameGenerator): distContents = {} def process(packageLists, extraPathElement, generateReleaseFile): for (distribution, component, architecture), packageList in packageLists.iteritems(): distComponents, distArchitectures = distContents.setdefault(distribution, ([], [])) distComponents.append(component) distArchitectures.append(architecture) def generateFilename(binaryPackage, filename): return filenameGenerator(distribution, component, architecture, binaryPackage, filename) indexPath = os.path.join( "dists", distribution, component, extraPathElement, "binary-%s" % architecture ) self._addIndexFile( files, os.path.join(indexPath, "Packages"), packageList.calculateIndexFile(architecture, generateFilename), ) if generateReleaseFile: self._addReleaseFile(files, indexPath, distribution, component, architecture) process(self._packageLists, "", generateReleaseFile=True) process(self._installerPackageLists, "debian-installer", generateReleaseFile=False) for (distribution, (components, architectures)) in distContents.iteritems(): parts = [] for field in ("Origin", "Label", "Suite", "Version", "Date", "Description"): if field in self._releaseFields: parts.append("%s: %s" % (field, self._releaseFields[field])) parts.append("Codename: %s" % distribution) parts.append("Architectures: %s" % " ".join(architectures)) parts.append("Components: %s" % " ".join(components)) files[os.path.join("dists", distribution, "Release")] = "\n".join(parts) def _collectSourcePackageFiles(self, files, directoryGenerator): for (distribution, component), packageList in self._sourcePackageLists.iteritems(): def generateDirectory(sourcePackage): return directoryGenerator(distribution, component, sourcePackage) indexPath = os.path.join("dists", distribution, component, "source") self._addIndexFile( files, os.path.join(indexPath, "Sources"), packageList.calculateIndexFile(files, generateDirectory) ) self._addReleaseFile(files, indexPath, distribution, component, "source") class PooledRepository(AutomaticRepository): def implCalculateFiles(self): files = {} def generatePoolFilename(distribution, component, architecture, binaryPackage, filename): fullname = os.path.join("pool", component, filename[0], filename) files[fullname] = "simulated contents of %s" % filename return fullname def generateSourceDirectory(distribution, component, sourcePackage): return os.path.join("dists", distribution, component, "source", sourcePackage["Section"]) self._collectPackageFiles(files, generatePoolFilename) self._collectSourcePackageFiles(files, generateSourceDirectory) return files class NonPooledRepository(AutomaticRepository): def implCalculateFiles(self): files = {} def generateFilename(distribution, component, architecture, binaryPackage, filename): fullname = os.path.join( "dists", distribution, component, "binary-%s" % architecture, binaryPackage["Section"], filename ) files[fullname] = "simulated contents of %s" % filename return fullname def generateSourceDirectory(distribution, component, sourcePackage): return os.path.join("dists", distribution, component, "source", sourcePackage["Section"]) self._collectPackageFiles(files, generateFilename) self._collectSourcePackageFiles(files, generateSourceDirectory) return files class TrivialRepository(AbstractRepository): pass class _PackageList: def __init__(self): self._packages = [] self._packagesByName = {} def addPackage(self, name, **kw): nameParts = name.split("_") if len(nameParts) > 1: name = nameParts[0] defaultVersion = nameParts[1] else: defaultVersion ="1.0" fields = { "Version" : defaultVersion, "Priority" : "optional", "Architecture" : "any", "Section" : "python", } fields.update(kw) self._packages.append((name, fields)) self._packagesByName.setdefault(name, {})[fields["Version"]] = fields def calculateIndexFile(self, defaultArchitecture, filenameGenerator): lines = [] for packageName, fields in self._packages: architecture = fields["Architecture"] if architecture != "all": architecture = defaultArchitecture lines.append("Package: %s" % packageName) lines.append("Architecture: %s" % architecture) lines.append("Filename: %s" % filenameGenerator(fields, "%s_%s_%s.%s" % ( packageName, fields["Version"], architecture, self.getExtension() ))) for fieldName, fieldValue in fields.iteritems(): lines.append("%s: %s" % (fieldName, fieldValue)) lines.append("") return "\n".join(lines) def getExtension(self): return "deb" def getPackage(self, packageName, version=None): packages = self._packagesByName[packageName] if len(packages) == 1 and version is None: return packages.values()[0] if version is None: raise RuntimeError("Multiple versions exist for '%s' but no version spectified" % packageName) return packages[version] def iterfields(self): for name, fields in self._packages: yield fields class _InstallerPackageList(_PackageList): def getExtension(self): return "udeb" class _SourcePackageList: def __init__(self): self._packages = {} def addPackage(self, name, **kw): fields = { "Version" : "1.0-1", "Section" : "python", "Format" : "1.0", } fields.update(kw) fileInfo = kw.pop("Files", None) if fileInfo is not None: filenames = [] for line in fileInfo.split("\n"): md5sum, size, filename = line.strip().split(" ") filenames.append(filename) fields["filenames"] = filenames self._packages[name] = fields def calculateIndexFile(self, files, directoryGenerator): lines = [] def addFile(filename, directory): contents = "simulated contents of %s" % filename files[os.path.join(directory, filename)] = contents return " %s %s %s" % ( hashlib.md5(contents).hexdigest(), len(contents), filename, ) for packageName, fields in self._packages.iteritems(): lines.append("Package: %s" % packageName) directory = directoryGenerator(fields) fields["Directory"] = directory for fieldName, fieldValue in fields.iteritems(): lines.append("%s: %s" % (fieldName, fieldValue)) lines.append("Files:") fullVersion = fields["Version"] upstreamVersion = fullVersion.split("-")[0] filenames = [] for suffix in ( "%s.dsc" % fullVersion, "%s.orig.tar.gz" % upstreamVersion, "%s.diff.gz" % fullVersion, ): filename="%s_%s" % (packageName, suffix) lines.append(addFile(filename, directory)) filenames.append(filename) lines.append("") fields["filenames"] = filenames return "\n".join(lines) def parseTagFile(filename): sectionMaps = [] f = open(filename, "r") try: for section in apt_pkg.TagFile(f): m = {} for key in section.keys(): m[key] = section[key] sectionMaps.append(m) return sectionMaps finally: f.close() debpartial-mirror/tests/__init__.py0000644000000000000000000000175111647020704014643 0ustar ########################################################################## # # __init__.py: defines this directory as the 'tests' package. # # Debian Partial Mirror is a tool to make complete and partial Debian mirrors. # See http://projetos.ossystems.com.br/debpartial-mirror/ for more information. # # ==================================================================== # Copyright (c) 2002-2005 O.S. Systems. All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # ######################################################################### # Author: Otavio Salvador import unittest import TestConfig import TestDownload import TestEndToEnd def suite(): suite = unittest.TestSuite() suite.addTest(TestConfig.suite()) suite.addTest(TestDownload.suite()) suite.addTest(TestEndToEnd.suite()) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') debpartial-mirror/tests/TestBase.py0000644000000000000000000000370511647020704014617 0ustar ########################################################################## # # __init__.py: defines this directory as the 'tests' package. # # Debian Partial Mirror is a tool to make complete and partial Debian mirrors. # See http://projetos.ossystems.com.br/debpartial-mirror/ for more information. # # ==================================================================== # Copyright (c) 2002-2005 O.S. Systems. All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # ######################################################################### # Author: Otavio Salvador from os.path import abspath, join, dirname import os import shutil import sys import tempfile import unittest class TestBase(unittest.TestCase): use_relative_paths = os.environ.has_key("USE_RELATIVE_PATHS") TEST_DIR = "tests" def setUp(self): self._tempDirs = [] self._tempFiles = [] def tearDown(self): for tempDir in self._tempDirs: shutil.rmtree(tempDir) for tempFile in self._tempFiles: if os.path.exists(tempFile): os.remove(tempFile) def aux_rel_path(self): if "aux" in os.listdir("."): return "aux" return "%s/aux" % self.TEST_DIR def aux_file(self, filename): return abspath(os.path.join(self.aux_rel_path(), filename)) current_dir = abspath(dirname(sys.argv[0])) if not current_dir.endswith(self.TEST_DIR): current_dir += "/" + self.TEST_DIR return join(current_dir, 'aux', filename) def createTempDirectory(self): if self.use_relative_paths: tempDir = tempfile.mkdtemp(prefix="dpmtest", dir=".") else: tempDir = tempfile.mkdtemp(prefix="dpmtest") self._tempDirs.append(tempDir) return tempDir def createTempFile(self): if self.use_relative_paths: tempFile = tempfile.mktemp(prefix="dpmtest", dir=".") else: tempFile = tempfile.mktemp(prefix="dpmtest") self._tempFiles.append(tempFile) return tempFile debpartial-mirror/tests/Makefile0000644000000000000000000000201311647020704014162 0ustar # debpartial-mirror - partial debian mirror package tool # (c) 2004 Otavio Salvador , Nat Budin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ### This makefile is to be used to test all needed features of ### debpartial-mirror system # Point the sources all: python tests.py USE_RELATIVE_PATHS=1 python tests.py clean: rm -f *.pyc *~ debpartial-mirror/tests/aux/0000755000000000000000000000000011647020704013323 5ustar debpartial-mirror/tests/aux/TestConfig-typecast.conf0000644000000000000000000000034311647020704020071 0ustar [GLOBAL] mirror_dir = /var/cache/debpartial-mirror/ architectures = i386 powerpc components = main distributions = stable get_suggests = false get_recommends = true get_provides = true get_packages = true get_sources = false debpartial-mirror/tests/aux/TestConfig-invalid-option-value.conf0000644000000000000000000000145011647020704022303 0ustar ;; ;; debpartial-mirror configuration file. ;; ;; ;; This is a good configuration file. ;; [GLOBAL] mirror_dir = /var/cache/debpartial-mirror/ architectures = i386 components = main distributions = stable get_suggests = don't get_recommends = true get_provides = true get_packages = true get_sources = false [sarge] server = http://ftp.debian.org/debian components = main distributions = sarge ;; Only get a subset of the packages in this source. filter = subsection:base priority:important [local_custom_packages] server = file:///var/lib/custom-packages components = main distributions = local resolve_deps_using = sarge ;;[my_custom_debian_distro] ;;backends = sarge sid_debian-installer local_custom_packages ;;name = sarge-with-sids-installer-and-some-other-stuff ;;filter_sarge = section:base debpartial-mirror/tests/aux/TestConfig-good.conf0000644000000000000000000000141511647020704017166 0ustar ;; ;; debpartial-mirror configuration file. ;; ;; ;; This is a good configuration file. ;; [GLOBAL] mirror_dir = /var/cache/debpartial-mirror/ architectures = i386 components = main distributions = stable get_suggests = true get_recommends = true get_provides = true get_packages = true get_sources = false [sarge] server = http://ftp.debian.org/debian components = main distributions = sarge ;; Only get a subset of the packages in this source. filter = subsection:base priority:important [local_custom_packages] server = file:///var/lib/custom-packages components = main distributions = local resolve_deps_using = sarge [my_custom_debian_distro] backends = sarge local_custom_packages name = sarge-with-some-other-stuff filter_sarge = name:myStuff field-Tag:suite::kde debpartial-mirror/tests/aux/TestConfig-section-types.conf0000644000000000000000000000161711647020704021050 0ustar ;; ;; debpartial-mirror configuration file. ;; ;; ;; This has the resolve_deps_using option in a merge backend, which is not ;; allowed. ;; [GLOBAL] mirror_dir = /var/cache/debpartial-mirror/ architectures = i386 components = main distributions = stable get_suggests = true get_recommends = true get_provides = true get_packages = true get_sources = false [sid_debian-installer] server = http://ftp.debian.org/debian components = main/debian-installer distributions = sid include_from_task = /usr/share/debian-cd/tasks/base-sarge exclude_from_task = /usr/share/debian-cd/tasks/exclude-sarge [local_custom_packages] server = file:///var/lib/custom-packages components = main distributions = local resolve_deps_using = sarge [my_custom_debian_distro] backends = sarge sid_debian-installer local_custom_packages name = sarge-with-sids-installer-and-some-other-stuff ;; Invalid resolve_deps_using = sarge debpartial-mirror/tests/aux/TestConfig-invalid-section.conf0000644000000000000000000000124511647020704021327 0ustar ;; ;; debpartial-mirror configuration file. ;; ;; ;; This has a section "bad_section" with an unresolvable type. ;; Missing 'server' option. ;; [GLOBAL] mirror_dir = /var/cache/debpartial-mirror/ architectures = i386 components = main distributions = stable get_suggests = true get_recommends = true get_provides = true [bad_section] components = main distributions = local [sarge] server = http://ftp.debian.org/debian components = main distributions = sarge filter = section:base priority:important [my_custom_debian_distro] backends = sarge sid_debian-installer local_custom_packages name = sarge-with-sids-installer-and-some-other-stuff filter_sarge = section:base debpartial-mirror/tests/aux/TestConfig-invalid-filter.conf0000644000000000000000000000210311647020704021142 0ustar ;; ;; debpartial-mirror configuration file. ;; ;; ;; This has a filter with some options that are not allowed ;; (in sid_debian-installer). ;; [GLOBAL] mirror_dir = /var/cache/debpartial-mirror/ architectures = i386 components = main distributions = stable get_suggests = true get_recommends = true get_provides = true get_packages = true get_sources = false [sarge] server = http://ftp.debian.org/debian components = main distributions = sarge filter = section:base priority:important [sid_debian-installer] server = http://ftp.debian.org/debian components = main/debian-installer distributions = sid filter = section:base kill:config this_filter:is_invalid include_from_task = /usr/share/debian-cd/tasks/base-sarge exclude_from_task = /usr/share/debian-cd/tasks/exclude-sarge [local_custom_packages] server = file:///var/lib/custom-packages components = main distributions = local resolve_deps_using = sarge [my_custom_debian_distro] backends = sarge sid_debian-installer local_custom_packages name = sarge-with-sids-installer-and-some-other-stuff filter_sarge = section:base debpartial-mirror/tests/aux/TestConfig-invalid-option.conf0000644000000000000000000000122511647020704021171 0ustar ;; ;; debpartial-mirror configuration file. ;; ;; ;; This has three invalid options in GLOBAL (xyzzy, user, and ;; i_cant_write_a_good_config_file). ;; [GLOBAL] ;; Bwahaha! I can put lots of weirdness into the configuration file. ;; Otavio and Nat will never catch me! xyzzy = plugh i_cant_write_a_good_config_file = true user = id10t ;; ... well, damn. :) mirror_dir = /var/cache/debpartial-mirror/ architectures = i386 components = main distributions = stable get_suggests = true get_recommends = true get_provides = true [sarge] server = http://ftp.debian.org/debian components = main distributions = sarge filter = section:base priority:important debpartial-mirror/tests/aux/TestConfig-missing-required.conf0000644000000000000000000000126111647020704021524 0ustar ;; ;; debpartial-mirror configuration file. ;; ;; ;; This is missing the architectes (required) option ;; [GLOBAL] mirror_dir = /var/cache/debpartial-mirror/ components = main distributions = stable get_suggests = true get_recommends = true get_provides = true get_packages = true get_sources = false [sarge] server = http://ftp.debian.org/debian components = main distributions = sarge filter = section:base priority:important [local_custom_packages] server = file:///var/lib/custom-packages components = main distributions = local resolve_deps_using = sarge [my_custom_debian_distro] backends = sarge local_custom_packages name = sarge-with-sids-installer-and-some-other-stuff debpartial-mirror/gdebpartial-mirror.py0000644000000000000000000000223611647020704015527 0ustar #!/usr/bin/python """ Tool to build partial debian mirrors (and more) """ # debpartial-mirror - partial debian mirror package tool # (c) 2006 Otavio Salvador # (c) 2006 Marco Presi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # ------- # Imports # ------- import gtk from debpartial_mirror_ui import Callbacks from debpartial_mirror_ui import GDebPartialMirror if __name__ == "__main__": ui = GDebPartialMirror.GDebPartialMirror("/etc/debpartial-mirror.conf") gtk.main() debpartial-mirror/TODO0000644000000000000000000000247411647020704012063 0ustar TODO list for debpartial-mirror ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ General * Provide a virtual repository where the application built a new Packages file to simulate a complete new repository (useful for PICAX); * Support to get Depends of package from a local repository (useful when work with not released yet packages); * Should work well with squid proxy * properly handle debian bug #284027: Change cpp use to include from debian-cd tasks too. * merging support for mirrors with variations on /pool: security.debian.org, backports.org, volatile.debian.net Post 0.3.0 release: * Port to libapt-front instead of libapt_pkg since the first is easier; * Command-line option to select which backends to hit during this particular update, so that you can schedule different updates at different times; * Automatic logging and statistics, possibly with sending mail to a specified address on each run; * support for multiple archive formats: http://web.freegeek.org/freekbox3, others. * option to specify server, dist and components with a single configuration option, similar to how mirrors are defined in sources.list Pre 0.3.0 release: * Fix clean command (it doesn't work) * Fix Packages.gz handling: update should downloading again if it has been changed on remote repository debpartial-mirror/debian/0000755000000000000000000000000011656010336012606 5ustar debpartial-mirror/debian/source/0000755000000000000000000000000011647020704014106 5ustar debpartial-mirror/debian/source/format0000644000000000000000000000001511647020704015315 0ustar 3.0 (native) debpartial-mirror/debian/changelog0000644000000000000000000002510111656010272014456 0ustar debpartial-mirror (0.3.1) unstable; urgency=low [ Martin Fuzzey ] * Apply second version of patch from Pino Toscano to user errno module rather than hardcoded constants (Closes: #640628) -- Otavio Salvador Mon, 07 Nov 2011 15:09:03 -0200 debpartial-mirror (0.3.0) unstable; urgency=low [ Martin Fuzzey ] * Apply patch from Pino Toscano to user errno module rather than hardcoded constants (Closes: #640628) * Apply patch from Andrew Suffield to use most recent of multiple versions (Closes: #645309) -- Otavio Salvador Mon, 17 Oct 2011 10:42:20 -0200 debpartial-mirror (0.2.99) unstable; urgency=low [ Martin Fuzzey ] * Fix exceptions in clean (Closes: #596672) -- Otavio Salvador Fri, 22 Oct 2010 19:01:17 -0200 debpartial-mirror (0.2.98) unstable; urgency=low [ Martin Fuzzey ] * python-apt 0.8 API transition (Closes: #572082) * Fix lintian warnings * Switch tests to python-mock (since python-pmock removed from Debian) [ Otavio Salvador ] * Update to dpkg-source 3.0 (native) format. -- Otavio Salvador Mon, 22 Mar 2010 11:08:39 -0300 debpartial-mirror (0.2.97) unstable; urgency=low [ Martin Fuzzey ] * Fixes - Apply patch from Alexander Inyukhin to fix SHA1 sums. Closes: #502642 - Avoid crash when release fields missing (Version..). Closes: #511106 - Allow mirroring multiple architectures. Closes: #497907 -- Otavio Salvador Thu, 22 Jan 2009 08:49:59 -0200 debpartial-mirror (0.2.96) unstable; urgency=low [ Otavio Salvador ] * Fix build-dependencies to please lintian. * Grab Release.gpg when mirroring. [ Martin Fuzzey ] * Fixes - resolve_deps_using now implemented. Closes: #389341 - Fix race condition in Download that sometimes caused files to be skipped - Old files now correctly removed from merge mirror - Some package fields were not copied by merge mirror - Mirrors with relative paths now cleaned properly - Check if we're on a tty before change terminal options. Closes: #389340 - Get all available dependencies even if some missing * New features - Add standalone config key to force fail if some dependencies unavailable - Support signing merge mirrors - Support automatic non pooled repositories - Support filters for merge mirrors - Add filters for arbitary package fields (field-XXX) - Add negative (exclude) filters on section, priority and name * Improvements - Better configuration error reporting * Internals - Add end to end tests - Use tabs for indentation [ Otavio Salvador ] * Add VCS-{Git,Browser} to debian/control -- Otavio Salvador Mon, 26 May 2008 21:20:01 -0300 debpartial-mirror (0.2.95) unstable; urgency=low [ Otavio Salvador ] * Check if we're on a tty before change terminal options. Closes: #389340 [ Tiago Bortoletto Vaz ] * Fix problem building architecture in Dists.py. Thanks to Ben Armstrong for reporting. Closes: #397475 -- Otavio Salvador Wed, 8 Nov 2006 19:43:07 -0200 debpartial-mirror (0.2.94) unstable; urgency=low [ Otavio Salvador ] * Applied patch from "Antonio S. de A. Terceiro" to fix a wrong variable name. Closes: #388465 * Applied patch from "Antonio S. de A. Terceiro" to fix the dependency checking of backends. Closes: #388463 [ Tiago Bortoletto Vaz ] * More changes in Pool.py, that didn't use full paths to compare unneeded files then removing needed ones. * Cleaning Pool.py code. -- Otavio Salvador Thu, 21 Sep 2006 13:46:36 -0300 debpartial-mirror (0.2.93) unstable; urgency=low [ Tiago Bortoletto Vaz ] * debian/rules - uncomment manpage generating in build target (we need manpage!) :) - using pysupport instead of pycentral. By using it, it's not necessary pycompat file anymore, either XS-Python-Version and XB-Python-Version in control file * debian/control - add docbook-to-man as build dependence - remove XS-Python-Version and XB-Python-Version fields - add build-depends python-support instead of python-central - update debhelper version to (>= 5.0.37.2) * debian/manpage.sgml - fix dhsection from 'net' to '1' - add '()' to separate Otavio's email from his name in the AUTHOR section * debian/compat - update from 4 to 5 * debpartial_mirror/Pool.py - fix a important problem about removing needed files after downloading them -- Otavio Salvador Sun, 17 Sep 2006 23:02:54 -0300 debpartial-mirror (0.2.92) unstable; urgency=low * Bump Standards-Version to 3.7.2: - Move cdbs and related dependencies to Build-Depends field. * Update to python policy 0.4.1: - add XS-Python-Version: current in source package; - add XB-Python-Version: ${python:Versions} in binary package; - set build-depends for python to python (>= 2.3.5-7) since we need it for pyversions; -- Otavio Salvador Fri, 23 Jun 2006 10:39:39 -0300 debpartial-mirror (0.2.91) unstable; urgency=low * Add python as build-depends. Closes: #361801. * Handle directory downloads properly. -- Otavio Salvador Mon, 5 Jun 2006 23:09:07 -0300 debpartial-mirror (0.2.90) unstable; urgency=low * This very is a rewrote of Debian Partial Mirror utility. We did it since the previous version has a lot of design issues that we wasn't able to solve without rethink it. Take care of upgrade of configuration file by your own since this package wasn't yet included in any stable release. You should see a full configuration file for reference in /etc/debpartial-mirror.conf -- Otavio Salvador Sat, 8 Apr 2006 15:49:35 -0300 debpartial-mirror (0.2.11) unstable; urgency=low * Vagrant Cascadian - fix typos. Closes: #290998, #290996 -- Otavio Salvador Sat, 30 Jul 2005 20:27:01 -0300 debpartial-mirror (0.2.10) unstable; urgency=low * Vagrant Cascadian - change "Error" to "Warning" in non-fatal cases. Closes: #284004 - flush output buffer so download percent displays sooner - minor wording corrections * Otavio Salvador - Change cpp use to include from debian-cd tasks too. Closes: #284027 -- Otavio Salvador Fri, 3 Dec 2004 22:13:28 -0200 debpartial-mirror (0.2.9) unstable; urgency=low * [Note: This is a bugfix release. All new features and major changes will be included on the 0.3.0 version. This version is current at development stage and you can take a look at it using SVN to do the checkout using: svn co \ svn://svn.debian.org/svn/partial-mirror/branches/rewrite \ debpartial-mirror Thanks!] * Fix some spelling errors. * Backport Vagrant Cascadian manpages fixes, did on rewrite branch. * Apply Vagrant Cascadian to have cleaner download output. Closes: #281303. * Check that local_directory= ends with '/'. Closes: #281399. -- Otavio Salvador Thu, 25 Nov 2004 18:33:20 -0200 debpartial-mirror (0.2.8) unstable; urgency=low * Otavio Salvador - Merge back the description improvement did by Ben Armstrong in rewrite branch. I only removed the part talking about use more then one source since this is not supported by this branch. - Change the URI on description to the website on Alioth. -- Otavio Salvador Fri, 2 Jul 2004 10:31:10 -0300 debpartial-mirror (0.2.7) unstable; urgency=low * Added new options to skip downloads of files and packages. See the manual page. * Added a simulate option (Closes: #254104). * Back to use docbook-to-man since this is more clear to me right now. * Improved the manpage. * Added a configuration option (get_provides) to allow to skip all provided packages (Closes: #254105). -- Otavio Salvador Wed, 16 Jun 2004 13:16:18 -0300 debpartial-mirror (0.2.6) unstable; urgency=low * debian/control: Add docbook-utils Build-Depends to fix FTBFS (Closes: #251605). * debian/rules: use docbook2man instead of docbook-to-man command. -- Otavio Salvador Mon, 31 May 2004 14:48:55 -0300 debpartial-mirror (0.2.5) unstable; urgency=low * Upload to unstable (did at DebConf4). * Fix manual page fallowing the tips of Per Olofsson (Closes: #250872). * Removed README.Debian by now since we don't have any interesting information for it (Closes: #250868). -- Otavio Salvador Tue, 4 May 2004 13:52:14 -0300 debpartial-mirror (0.2.4) experimental; urgency=low * src/RemotePackages.py: use Replaces to get another possible solution to dependencies. -- Otavio Salvador Tue, 4 May 2004 02:00:43 -0300 debpartial-mirror (0.2.3) experimental; urgency=low * src/RemotePackages.py: fix virtual-package processing. Now this identify all virtual-package. * src/debpartial-mirror.in: changed the label about when was built. More short now. -- Otavio Salvador Tue, 4 May 2004 01:58:53 -0300 debpartial-mirror (0.2.2) experimental; urgency=low * Removed the TODO item about apt-proxy since current development version solved the problem. * debian/control: fix description spelling errors. * debian/manpage.sgml: fix spelling errors. * src/Config.py: changed getExclude member to getExcludes. * src/debpartial-mirror.in, debian/rules: added information about when was built. -- Otavio Salvador Tue, 4 May 2004 01:13:49 -0300 debpartial-mirror (0.2.1) experimental; urgency=low * Bug fix: "debpartial-mirror: [wishlist] Add support for a exclude-packages configuration", thanks to Gustavo R. Montesino (Closes: #247043). This was already supported but now I have documented it in configuration file for example. -- Otavio Salvador Sun, 2 May 2004 23:00:23 -0300 debpartial-mirror (0.2.0) experimental; urgency=low * Added support to resolve virtual packages. -- Otavio Salvador Fri, 30 Apr 2004 20:13:01 -0300 debpartial-mirror (0.1.1) experimental; urgency=low * Remove wrong information while processing debian-cd tasks. * Include better examples in configuration file. * RemotePackages.py (RemotePackages.__selectPackage): removed unused parameter. -- Otavio Salvador Fri, 30 Apr 2004 13:39:45 -0300 debpartial-mirror (0.1) experimental; urgency=low * Initial Release. -- Otavio Salvador Thu, 22 Apr 2004 22:33:48 -0300 debpartial-mirror/debian/manpage.sgml0000644000000000000000000001655011647020704015111 0ustar manpage.1'. You may view the manual page with: `docbook-to-man manpage.sgml | nroff -man | less'. A typical entry in a Makefile or Makefile.am is: manpage.1: manpage.sgml docbook-to-man $< > $@ The docbook-to-man binary is found in the docbook-to-man package. Please remember that if you create the nroff version in one of the debian/rules file targets (such as build), you will need to include docbook-to-man in your Build-Depends control field. --> Otavio"> Salvador"> março 26, 2004"> 1"> otavio@debian.org"> DEBPARTIAL-MIRROR"> Debian"> GNU"> GPL"> ]>
&dhemail;
&dhfirstname; &dhsurname; 2003 &dhusername; &dhdate;
&dhucpackage; &dhsection; &dhpackage; debpartial-mirror is a program to generate partial Debian packages archives mirrors. apt-get update upgrade dselect-upgrade install pkg remove pkg source pkg build-dep pkg check clean autoclean DESCRIPTION &dhpackage; is a command-line tool to generate partial Debian packages archives mirrors. It is designed to work (in particular) in Custom Debian Distributions framework, as an aid to create customization of debian systems. Partial mirrors can be generated from different apt repositories (local created or remote ones) and can contain binary as well as sources packages. Generated partial mirrors have the structure of standard apt repositories: please referr to APT documentation for explanation about this. &dhpackage; uses a configuration file to determine which packages, are to be downloaded and monitored to generate a local partial mirror. Packages can be specified by regular expressions, by list or by mirrors sections. Default configuration file is /etc/debpartial-mirror.conf. &dhpackage; accept commands in the same way apt-get does, and unless the -h or --help is given, one of the commands below must be presented. update upgrade all OPTIONS This program follows the usual &gnu; command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. Show summary of options. Show version of program. Select the confiruration file. Do nothing, only simulate all. Actually it is not implemented Skip downloading of files. Actually it is not implemented Skip downloading of packages. Actually it is not implemented AUTHOR This manual page was written by &dhusername; (&dhemail;). Permission is granted to copy, distribute and/or modify this document under the terms of the &gnu; General Public License, Version 2 any later version published by the Free Software Foundation. On Debian systems, the complete text of the GNU General Public License can be found in /usr/share/common-licenses/GPL.
debpartial-mirror/debian/copyright0000644000000000000000000000173411647020704014546 0ustar This package was debianized by Otavio Salvador on Fri, 26 Mar 2004 15:51:51 -0300. Upstream Author: Otavio Salvador Copyright 2004 Otavio Salvador This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 dated June, 1991. This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. On Debian systems, the complete text of the GNU General Public License can be found in `/usr/share/common-licenses/GPL-2'. debpartial-mirror/debian/docs0000644000000000000000000000003411647020704013456 0ustar TODO doc/HACKING doc/README debpartial-mirror/debian/rules0000755000000000000000000000120311647020704013662 0ustar #!/usr/bin/make -f DEB_PYTHON_SYSTEM=pysupport include /usr/share/cdbs/1/rules/debhelper.mk include /usr/share/cdbs/1/class/python-distutils.mk configure/debpartial-mirror:: # Fill the vars sed -i "s/version_str = .*/version_str = '$(shell dpkg-parsechangelog | grep '^Version:' | awk '{ print $$2; }')'/g" debpartial-mirror sed -i "s/version = .*,/version = '$(shell dpkg-parsechangelog | grep '^Version:' | awk '{ print $$2; }')',/g" setup.py build/debpartial-mirror:: # Build the manual page docbook-to-man $(CURDIR)/debian/manpage.sgml > $(CURDIR)/debpartial-mirror.1 clean:: rm -f src/*.pyc tests/*.pyc \ debpartial-mirror.1 debpartial-mirror/debian/examples0000644000000000000000000000003311647020704014343 0ustar etc/debpartial-mirror.conf debpartial-mirror/debian/compat0000644000000000000000000000000211647020704014004 0ustar 5 debpartial-mirror/debian/control0000644000000000000000000000210211647020704014204 0ustar Source: debpartial-mirror Section: net Priority: extra Maintainer: Otavio Salvador Uploaders: Martin Fuzzey Build-Depends: debhelper (>= 5.0.37.2), cdbs (>= 0.4.41), docbook-to-man, python, python-support (>= 0.3) Standards-Version: 3.8.4 VCS-Git: git://projetos.ossystems.com.br/git/debpartial-mirror.git VCS-Browser: http://projetos.ossystems.com.br/repositories/show/debpartial-mirror Package: debpartial-mirror Architecture: all Depends: ${python:Depends}, ${misc:Depends}, python-apt (>= 0.7.93.2~), python-pycurl, python-cdd (>= 0.0.8), gnupg Description: tools to create partial Debian mirrors Mirroring of partial Debian repositories is easy with the debpartial-mirror tool. . With it, you can mirror selected sections, priorities, packages, virtual packages, or even files and directories matching regular expressions. Packages may be drawn from any number of sources, with dependencies for each source resolved from whichever other sources you specify. Homepage: http://projetos.ossystems.com.br/projects/show/debpartial-mirror debpartial-mirror/debian/debpartial-mirror.manpages0000644000000000000000000000002411647020704017736 0ustar debpartial-mirror.1 debpartial-mirror/etc/0000755000000000000000000000000011647020704012137 5ustar debpartial-mirror/etc/debpartial-mirror.conf0000644000000000000000000000614111647020704016427 0ustar ;; ;; debpartial-mirror configuration file. ;; [GLOBAL] ;; Show debug information? ;debug = DEBUG ;; Mirror destination directory mirror_dir = ../debpartial-mirror/ ;; Which architectures should I download? architectures = i386 ;; What should I look for, by default? components = main distributions = stable ;; What should I get? get_suggests = true get_recommends = true get_provides = true get_sources = false get_packages = true ;; Here is our first backend. It mirrors a subset of packages from the ;; Debian official repositories. [sarge] ;; Where do we get the packages from? server = http://ftp.debian.org/debian ;; Since we specify components and distributions in this section, what we ;; specify here overrides the settings in [DEFAULT]. components = main distributions = sarge ;; Only get a subset of the packages in this source. filter = subsection:base priority:important ;; And get the source packages as well for this backend. get_sources = false [sarge-security] ;; also get security updates for sarge server = http://security.debian.org/ components = main distributions = sarge/updates filter = subsection:base priority:important resolve_deps_using = sarge ;; Here is another backend. This one will get all the debian-installer ;; packages from the unstable distribution (sid). ;;[sid_debian-installer] ;;server = http://ftp.debian.org/debian ;;components = main/debian-installer ;;distributions = sid ;; You can use debian-cd tasks to include or exclude a subset of the ;; packages. ;;filter = include-from:/usr/share/debian-cd/tasks/base-sarge exclude-from:/usr/share/debian-cd/tasks/exclude-sarge ;; Let's only get the source packages, not the binary ones. #get_sources = false ;;get_packages = false ;; This backend is a local repository, as you can see from the use of the ;; file:// URL. The idea is that we have a set of custom-made packages ;; stored on the local computer. ;;[local_custom_packages] ;;server = file:///var/lib/custom-packages ;;components = main ;;distributions = local ;; These packages depend on Debian official packages. We will use the ;; "sarge" backend (above) to satisfy these dependencies. ;;resolve_deps_using = sarge ;; Here is a merging backend. It uses the backends we specify above to ;; create a custom distribution that provides all the packages in each ;; backend. This will be created using hard links to the package files ;; in each backend directory. ;;[my_custom_debian_distro] ;;backends = sarge sid_debian-installer local_custom_packages ;;name = sarge-with-sids-installer-and-some-other-stuff ;;filter_sarge = section:base ;;[backports] ;;server = http://backports.org/debian ;; note: sarge is not yet present at backports.org ;;distributions=woody ;;components = all ;; backports.org defines each package as a component, so you can specify ;; individual packages only: ;;components = mailman spamassassin postfix postgrey ;;resolve_deps_using = sarge ;;[volatile] ;;server=http://volatile.debian.net/debian-volatile ;;distributions=sarge/volatile ;;components=main ;;resolve_deps_using = sarge ;;only grab the clamav package (and things that it depends on) ;;filter=name:clamav debpartial-mirror/debpartial_mirror_ui/0000755000000000000000000000000011647020704015562 5ustar debpartial-mirror/debpartial_mirror_ui/README.txt0000644000000000000000000000030111647020704017252 0ustar Developers Notes ---------------- UI is realized with Glade-2. As one of the widgets is a GnomeApp, in order to open and edit the glade files, you need to install both glade and glade-gnome. debpartial-mirror/debpartial_mirror_ui/Callbacks.py0000644000000000000000000000561711647020704020024 0ustar import gtk def on_pop_up_update_activate(popup, app): """ Execute when pop-up Update item is selected""" print "Update backend pressed", app def on_pop_up_upgrade_activate (popup, app): """ Execute when pop-up Upgrade item is selected """ print "Upgrade backend pressed", app def on_b_update_clicked (button, app): """ Executed when Update toolbar button is clicked""" print "Update selected backends clicked", app def do_popup(app, event): """open popup """ print "Opening popup" menu = app.xml.get_widget ("backend_pop_up_menu") menu.connect ("deactivate", gtk.Widget.hide) gtk.Menu.popup(menu, None, None, None, event.button, event.time) def on_backend_treeview_event(b_treeview, event, app): """Check for events """ if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: do_popup (app, event) return False def backend_toggled_cb (toggle, path, app): """ Change the toggle status of the selected backend. If a class of backends is choosen (mirrors, merges,..) it toggles all the backends of that category toggle: is the toggle widget that has been toggled path: represent the item path app: main app """ status = toggle.get_active() iter = app.model.get_iter (path) app.model.set (iter, app.COLUMN_SEL, (not status)) if app.model.iter_has_child (iter): child = app.model.iter_children (iter) while (isinstance(child, gtk.TreeIter)): app.model.set (child, app.COLUMN_SEL, (not status)) child = app.model.iter_next(child) app.b_treeview.set_model (app.model) #Toolbar def quit (): gtk.main_quit() def on_new_activate (obj, app): """Exit the application """ print app print "File->New called" def on_open_activate (obj, app): """ Open a configuration file """ print "File->Open called" open_dialog = gtk.FileChooserDialog("test", action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT)) r = open_dialog.run() if r == gtk.RESPONSE_ACCEPT: print open_dialog.get_filename(), "selected" del app.conffile del app.cnf app.conffile = open_dialog.get_filename() app.parse_conffile () app.update_backends_treeview () elif r == gtk.RESPONSE_CANCEL: pass open_dialog.destroy() def on_save_activate (obj, app): """Exit the application """ print app print "File->Save called" def on_clean_all_activate (obj, app): """Exit the application """ print app print "File->Clean called" def on_quit_activate (app): """Exit the application """ print app print "File->Exit called" quit() def backend_update (button, app): """ Update all selected backends """ debpartial-mirror/debpartial_mirror_ui/interface/0000755000000000000000000000000011647020704017522 5ustar debpartial-mirror/debpartial_mirror_ui/interface/backend_ui.gladep0000644000000000000000000000043111647020704022762 0ustar Backend_ui backend_ui FALSE debpartial-mirror/debpartial_mirror_ui/interface/ui.glade0000644000000000000000000003711411647020704021143 0ustar True gdebpartial-mirror GTK_WINDOW_TOPLEVEL GTK_WIN_POS_NONE False 640 480 True False True False False GDK_WINDOW_TYPE_HINT_NORMAL GDK_GRAVITY_NORTH_WEST True False True True True True GTK_SHADOW_NONE True GTK_PACK_DIRECTION_LTR GTK_PACK_DIRECTION_LTR True _File True True gtk-new True True gtk-open True True gtk-save True True gtk-save-as True True True gtk-quit True True _Modifica True True gtk-cut True True gtk-copy True True gtk-paste True True gtk-clear True True True Pr_oprietà True True True gtk-preferences True True _Visualizza True True Aiuto True True gtk-help True True gtk-info True BONOBO_DOCK_TOP 0 0 0 BONOBO_DOCK_ITEM_BEH_EXCLUSIVE|BONOBO_DOCK_ITEM_BEH_NEVER_VERTICAL|BONOBO_DOCK_ITEM_BEH_LOCKED True GTK_SHADOW_OUT True GTK_ORIENTATION_HORIZONTAL GTK_TOOLBAR_BOTH True True True Nuovo file gtk-new True True False False True True Apri file gtk-open True True False False True True Salve file gtk-save True True False False True True True True True False False True gtk-clear True True False False True True gtk-refresh True True False False True BONOBO_DOCK_TOP 1 0 0 BONOBO_DOCK_ITEM_BEH_EXCLUSIVE 1 True True True create_backend_browser 0 0 Mon, 25 Sep 2006 22:31:43 GMT True False True Click on a property to select it create_configuration_editor 0 0 Sun, 24 Sep 2006 22:21:53 GMT True True 0 True True True True True 0 True True True Update Selected Backends Update True True Upgrade selected Backends Upgrade True debpartial-mirror/debpartial_mirror_ui/interface/ui.gladep0000644000000000000000000000045111647020704021315 0ustar gdebpartial-mirror gdebpartial-mirror FALSE debpartial-mirror/debpartial_mirror_ui/interface/backend_ui.glade0000644000000000000000000001206511647020704022610 0ustar True window1 GTK_WINDOW_TOPLEVEL GTK_WIN_POS_NONE False True False True False False GDK_WINDOW_TYPE_HINT_NORMAL GDK_GRAVITY_NORTH_WEST True False True False 0 True GTK_ORIENTATION_HORIZONTAL GTK_TOOLBAR_BOTH True True True gtk-clear True True False False True True gtk-refresh True True False False True True gtk-execute True True False False True True gtk-stop True True False False True 0 False False True True GTK_POLICY_ALWAYS GTK_POLICY_ALWAYS GTK_SHADOW_IN GTK_CORNER_TOP_LEFT True True True False False True False False False 0 True True True True 0 False False debpartial-mirror/debpartial_mirror_ui/ConfigEditor.py0000644000000000000000000001565211647020704020521 0ustar # debpartial_mirror_ui - User Interface for partial debian mirror package tool # (c) 2006 Otavio Salvador # (c) 2006 Marco Presi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from gtk import VBox, HBox, Notebook, VPaned, Label, \ TreeView, TreeIter, TreeViewColumn, TreeStore, \ CellRendererText, Frame, SHADOW_IN, SELECTION_SINGLE import gobject import ConfigParser from debpartial_mirror import Config class PropertyEditor (TreeView): """ A TreeView with property/value pairs """ __gsignals__ = dict(property_changed=(gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,))) def __init__ (self, cnf): super (PropertyEditor, self).__init__() self.cnf = cnf self.set_property ('visible', True) self.set_property ('can_focus', True) self.set_property ('headers_visible', True) self.set_property ('enable_search', True) self.set_property ('reorderable', True) self.set_property ('rules_hint',False) self.set_property ('fixed_height_mode', False) self.set_property ('hover_selection', False) self.set_property ('hover_expand', False) (self.COLUMN_PROP, self.COLUMN_VALUE) = range(2) self.model = TreeStore (gobject.TYPE_STRING, gobject.TYPE_STRING) renderer = CellRendererText () renderer.set_property ("xalign", 0.0) column = TreeViewColumn('Key', renderer, text=self.COLUMN_PROP) column.set_clickable(False) column.set_sort_column_id(self.COLUMN_PROP) self.append_column(column) column = TreeViewColumn('Value', renderer, text=self.COLUMN_VALUE) column.set_clickable(False) column.set_sort_column_id(self.COLUMN_VALUE) self.append_column(column) select = self.get_selection () select.set_mode (SELECTION_SINGLE) select.connect ('changed', self.property_selected_cb) self.expand_all() self.set_rules_hint(True) self.show_all() def property_selected_cb (self, selection): (model, iter) = selection.get_selected () if isinstance (iter, TreeIter): print "%s selected\n" % model.get_value (iter, self.COLUMN_PROP) self.emit ('property_changed', model.get_value (iter, self.COLUMN_PROP)) def update_property_editor (self, b_name): """ A method to update the propety/value pair after that a backend is selected """ print "update_property_editor: %s" % b_name try: options = self.cnf.options(b_name) self.model.clear () for b in options: print "Adding %s" % b self.iter = self.model.append (None) self.model.set (self.iter, self.COLUMN_PROP, b, self.COLUMN_VALUE, self.cnf.get_option(b, b_name)) except ConfigParser.NoSectionError: # If the selected row was not a backend, we simply ignore it if b_name == "Global": self.update_property_editor ('GLOBAL') self.set_model (self.model) class PropertyDescriptor (HBox): """A display widget that shows a description of the selected property """ prop = {'mirror_dir': 'Filesystem path where the partial mirror is built', 'components': 'Specify which archives sections should be mirrored', 'get_packages':'Add description', 'distributions': 'list of distributions (stable, testomg,..) to mirror', 'get_provides': 'Add description', 'get_recommends': 'Add description', 'architectures': 'list of architectures to mirrors', 'get_suggests': 'Add description', 'get_sources': 'Add description'} def __init__ (self, debug=False): super (PropertyDescriptor, self).__init__ () self._debug_mode = debug #self._default_msg = Label("Select a config file section.") self._prop_des = "" self._prop_name = "" self.prop_name_label = Label ("Name:") self.prop_desc_label = Label ("Description:") self.prop_name_text = Label () self.prop_desc_text = Label () label_box = VBox () desc_box = VBox () label_box.pack_start (self.prop_name_label) label_box.pack_start (self.prop_desc_label) desc_box.pack_start (self.prop_name_text) desc_box.pack_start (self.prop_desc_text) self.pack_start (label_box) self.pack_start (desc_box) self.show_all () def update_property_descriptor (self, w, prop_name): """ Modify the labels to describe the property selected in the configuration editor""" print "update_property_descriptor: %s" % prop_name try: prop_desc = self.prop[prop_name] self.prop_name_text.set_text ("%s" % prop_name) self.prop_desc_text.set_text ("%s" % prop_desc) except KeyError, e: print "Errore %s" % e class ConfigEditor (VPaned): """A widget to read/edit the configuration files """ def __init__ (self, cnf, debug=False): super (ConfigEditor, self).__init__() self._debug_mode = debug self.l = PropertyDescriptor () self.p = PropertyEditor (cnf) self.p.update_property_editor ('GLOBAL') #self.update_label (self.l) pframe = Frame () lframe = Frame () pframe.set_shadow_type (SHADOW_IN) lframe.set_shadow_type (SHADOW_IN) pframe.add (self.p) lframe.add (self.l) self.add1 (pframe) self.add2 (lframe) self.p.connect ('property_changed', self.l.update_property_descriptor) self.show_all () def update_property_editor (self, b_name): self.p.update_property_editor (b_name) def update_label (self, l): """ Change the label text """ l.set_text ('Select a config file section.') debpartial-mirror/debpartial_mirror_ui/BackendBrowser.py0000644000000000000000000001642111647020704021033 0ustar # debpartial_mirror_ui - User Interface for partial debian mirror package tool # (c) 2006 Otavio Salvador # (c) 2006 Marco Presi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #TODO: Create a CellRenderer that contains both the toggle button and the text. import gobject from gtk import VBox, Label, TreeView, \ TreeIter, TreeViewColumn, TreeStore, \ CellRendererToggle, CellRendererText, \ gdk, Frame, SHADOW_IN, SELECTION_SINGLE from debpartial_mirror import Backend from debpartial_mirror import Config class BackendBrowser(VBox): """ A widget to browse Backend configuration """ __gsignals__ = dict(backend_changed=(gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,))) def __init__(self, cnf, debug=True): super(BackendBrowser, self).__init__() self._debug_mode = debug self.l = Label ('Backend Browser') bframe = Frame () self.b = TreeView () self.read_backends (cnf) (self.COLUMN_SEL, self.COLUMN_NAME) = range(2) self.create_browser (self.b) bframe.add (self.b) self.pack_start (self.l, False, False, 5) self.pack_start (bframe, True) self.show_all() def _get_backends (self, cnf, type): """Get the backend list from config file""" backend = [] for b in cnf.get_backends ()[type]: backend.append(Backend.MirrorBackend (b.section, cnf)) return backend def _get_mirrors (self, cnf): return self._get_backends (cnf, 0) def _get_merges (self, cnf): return self._get_backends (cnf, 1) def read_backends (self, cnf): """Read the backends defined in the conf file """ self.mirrors = self._get_mirrors(cnf) self.merges = self._get_merges(cnf) def create_browser (self, b): ''' Create a TreeView that allows to browse all the backend defined in the Config file''' b.set_property ('visible', True) b.set_property ('can_focus', True) b.set_property ('headers_visible', True) b.set_property ('enable_search', True) b.set_property ('reorderable', True) b.set_property ('rules_hint',False) b.set_property ('fixed_height_mode', False) b.set_property ('hover_selection', False) b.set_property ('hover_expand', False) b.connect ("event", self.on_b_event) model = TreeStore (gobject.TYPE_BOOLEAN, gobject.TYPE_STRING) self.update_backends_treeview (model) b.set_model (model) #column for backend selection toggle = CellRendererToggle () toggle.set_property ("activatable", True) toggle.connect ('toggled', self.backend_toggled_cb, model) text = CellRendererText () text.set_property ("xalign", 0.0) col = TreeViewColumn() col.pack_start (toggle, False) col.pack_start (text, True) col.add_attribute (toggle, 'active', self.COLUMN_SEL) col.add_attribute (text, 'markup', self.COLUMN_NAME) col.set_clickable (True) col.set_sort_column_id (self.COLUMN_NAME) b.append_column (col) select = b.get_selection () select.set_mode (SELECTION_SINGLE) select.connect ('changed', self.backend_selection_changed_cb) b.expand_all() b.set_rules_hint (True) b.set_headers_visible (False) b.show_all() def update_backends_treeview (self, model): """ Update the browser """ model.clear () global_iter = model.append (None) model.set (global_iter, self.COLUMN_NAME, "Global") mirrors_iter = model.append(None) model.set (mirrors_iter, self.COLUMN_SEL, False, self.COLUMN_NAME, "Mirrors") merges_iter = model.append(None) model.set (merges_iter, self.COLUMN_SEL, False, self.COLUMN_NAME, "Merges") for i in self.mirrors: mirrors_list_iter = model.append (mirrors_iter) model.set (mirrors_list_iter, #self.COLUMN_SEL, None, self.COLUMN_NAME, i._name) if len(self.mirrors) == 0: mirrors_list_iter = model.append (mirrors_iter) model.set (mirrors_list_iter, #self.COLUMN_SEL, None, self.COLUMN_NAME, "No Mirrors defined") for i in self.merges: merges_list_iter = model.append (merges_iter) model.set (merges_list_iter, #self.COLUMN_SEL, None, self.COLUMN_NAME, i._name) if len(self.merges) == 0: merges_list_iter = model.append (merges_iter) model.set (merges_list_iter, #self.COLUMN_SEL, None, self.COLUMN_NAME, "No Merges defined") def backend_toggled_cb (self, toggle, path, model): """ Change the toggle status of the selected backend. If a class of backends is choosen (mirrors, merges,..) it toggles all the backends of that category toggle: is the toggle widget that has been toggled path: represent the item path app: main app """ status = toggle.get_active() iter = model.get_iter (path) model.set (iter, self.COLUMN_SEL, (not status)) if model.iter_has_child (iter): child = model.iter_children (iter) while (isinstance(child, TreeIter)): model.set (child, self.COLUMN_SEL, (not status)) child = model.iter_next(child) self.b.set_model (model) def backend_selection_changed_cb (self, selection): """ When a backend is selected, we show its properties in the property treeview.""" print self print selection (model, iter) = selection.get_selected () if isinstance (iter, TreeIter): print "%s selected\n" % model.get_value (iter, self.COLUMN_NAME) self.emit ('backend_changed', model.get_value (iter, self.COLUMN_NAME)) def on_b_event(self, treeview, event): if event.type == gdk.BUTTON_PRESS and event.button == 3: self.do_popup (event) def do_popup(self, event): """open popup """ print "Opening popup" debpartial-mirror/debpartial_mirror_ui/tests/0000755000000000000000000000000011647020704016724 5ustar debpartial-mirror/debpartial_mirror_ui/tests/testConfigEditor.py0000644000000000000000000000102311647020704022546 0ustar from os import sys sys.path.append ('../') from gtk import Window, main, main_quit from ConfigEditor import * from debpartial_mirror import Config, Backend cnf = Config.Config('/etc/debpartial-mirror.conf') mirrors = [] merges = [] for b in cnf.get_backends()[0]: mirrors.append(Backend.MirrorBackend(b.section,cnf)) for b in cnf.get_backends()[1]: merges.append(Backend.MirrorBackend(b.section,cnf)) w = Window() bb = ConfigEditor (cnf, debug = True) w.add(bb) w.show_all() w.connect('destroy', main_quit) main() debpartial-mirror/debpartial_mirror_ui/tests/testBackendBrowser.py0000644000000000000000000000105311647020704023070 0ustar from os import sys sys.path.append ('../') from gtk import Window, main, main_quit from BackendBrowser import BackendBrowser from debpartial_mirror import Config, Backend cnf = Config.Config('/etc/debpartial-mirror.conf') #mirrors = [] #merges = [] #for b in cnf.get_backends()[0]: # mirrors.append(Backend.MirrorBackend(b.section,cnf)) #for b in cnf.get_backends()[1]: # merges.append(Backend.MirrorBackend(b.section,cnf)) w = Window() bb = BackendBrowser (cnf, debug = True) w.add(bb) w.show_all() w.connect('destroy', main_quit) main() debpartial-mirror/debpartial_mirror_ui/TODO0000644000000000000000000000036111647020704016252 0ustar * add a routine to retrive selected backend, in order to perform actions on them (update/upgrade/....) * add routine to start update/upgrade on selected backends * define a class in order to output download progress of each backend debpartial-mirror/debpartial_mirror_ui/__init__.py0000644000000000000000000000134611647020704017677 0ustar ########################################################################## # # __init__.py: defines this directory as 'debpartial_mirror_ui' package. # # debpartial_mirror is an application to make easier to build full or # partial mirrors from Debian or compatible archives. # See http://partial-mirror.alioth.debian.org/ for more information. # # ==================================================================== # Copyright (c) 2002-2005 OS Systems. All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # ######################################################################### # Author: Otavio Salvador debpartial-mirror/debpartial_mirror_ui/GDebPartialMirror.py0000644000000000000000000001004611647020704021446 0ustar # debpartial_mirror_ui - User Interface for partial debian mirror package tool # (c) 2006 Otavio Salvador # (c) 2006 Marco Presi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import gnome import gtk.glade import gobject from BackendBrowser import BackendBrowser from ConfigEditor import ConfigEditor #for debuggin purpose from debpartial_mirror import Backend from debpartial_mirror import Config from debpartial_mirror import Download from BackendBrowser import BackendBrowser from Callbacks import * name = "gdebpartial-mirror" version = "0.0.1" app_glade_file = "debpartial_mirror_ui/interface/ui.glade" backend_glade_file = "debpartial_mirror_ui/interface/backend_ui.glade" cnf_file = "/etc/debpartial-mirror.conf" class GDebPartialMirror: """ A class that represent the main interface """ def __init__ (self, conffile): self.app = gnome.program_init (name, version) self.cnf = Config.Config (cnf_file) gtk.glade.set_custom_handler (self.get_custom_handler) self.xml = gtk.glade.XML (app_glade_file) self.debug_mode = True auto_signals = {"on_new_activate": (on_new_activate, self), "on_open_activate": (on_open_activate, self), "on_save_activate": (on_save_activate, self), "on_clean_all_activate": (on_clean_all_activate, self), "on_quit_activate": on_quit_activate, "on_backend_treeview_event": (on_backend_treeview_event, self), "on_pop_up_update_activate": (on_pop_up_update_activate, self), "on_pop_up_upgrade_activate": (on_pop_up_upgrade_activate,self), "on_b_update_clicked": (on_b_update_clicked, self), "on_backend_selection_change": (self.on_backend_selection_change)} # self.create_backends_treeview () self.xml.signal_autoconnect (auto_signals) #self.b.connect ('backend_changed', self.c.update_property_editor) def get_custom_handler(self, glade, function_name, widget_name, str1, str2, int1, int2): """ Generic handler for creating custom widgets, used to enable custom widgets. The custom widgets have a creation function specified in design time. Those creation functions are always called with str1,str2,int1,int2 as arguments, that are values specified in design time. This handler assumes that we have a method for every custom widget creation function specified in glade. If a custom widget has create_foo as creation function, then the method named create_foo is called with str1,str2,int1,int2 as arguments. """ handler = getattr(self, function_name) return handler(str1, str2, int1, int2) def create_backend_browser (self, str1, str2, int1, int2): self.b = BackendBrowser (self.cnf) return self.b def create_configuration_editor (self, str1, str2, int1, int2): self.c = ConfigEditor (self.cnf) return self.c def on_backend_selection_change (self, backend, b_name): """ When a backend is selected, we show its properties in the property treeview.""" self.c.update_property_editor (b_name)