]> git.mxchange.org Git - quix0rs-apt-p2p.git/blob - AptPackages.py
Fix the architecture problem in the AptPackages config system.
[quix0rs-apt-p2p.git] / AptPackages.py
1 #
2 # Copyright (C) 2002 Manuel Estrada Sainz <ranty@debian.org>
3 #
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of version 2.1 of the GNU Lesser General Public
6 # License as published by the Free Software Foundation.
7 #
8 # This library is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 # Lesser General Public License for more details.
12 #
13 # You should have received a copy of the GNU Lesser General Public
14 # License along with this library; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17 import apt_pkg, apt_inst, sys, os, stat
18 from os.path import dirname, basename
19 import re, shelve, shutil, fcntl
20 from twisted.internet import process
21 from twisted.python import log
22 import copy, UserDict
23 from twisted.trial import unittest
24
25 aptpkg_dir='.apt-dht'
26 apt_pkg.init()
27
28 class AptDpkgInfo(UserDict.UserDict):
29     """
30     Gets control fields from a .deb file.
31
32     And then behaves like a regular python dictionary.
33
34     See AptPackages.get_mirror_path
35     """
36
37     def __init__(self, filename):
38         UserDict.UserDict.__init__(self)
39         try:
40             filehandle = open(filename);
41             try:
42                 self.control = apt_inst.debExtractControl(filehandle)
43             finally:
44                 # Make sure that file is always closed.
45                 filehandle.close()
46         except SystemError:
47             log.msg("Had problems reading: %s"%(filename))
48             raise
49         for line in self.control.split('\n'):
50             if line.find(': ') != -1:
51                 key, value = line.split(': ', 1)
52                 self.data[key] = value
53
54 class PackageFileList(UserDict.DictMixin):
55     """
56     Manages a list of package files belonging to a backend
57     """
58     def __init__(self, backendName, cache_dir):
59         self.cache_dir = cache_dir
60         self.packagedb_dir = cache_dir+'/'+ aptpkg_dir + \
61                            '/backends/' + backendName
62         if not os.path.exists(self.packagedb_dir):
63             os.makedirs(self.packagedb_dir)
64         self.packages = None
65         self.open()
66
67     def open(self):
68         if self.packages is None:
69             self.packages = shelve.open(self.packagedb_dir+'/packages.db')
70
71     def close(self):
72         if self.packages is not None:
73             self.packages.close()
74
75     def update_file(self, filename, cache_path, file_path):
76         """
77         Called from apt_proxy.py when files get updated so we can update our
78         fake lists/ directory and sources.list.
79         """
80         if filename=="Packages" or filename=="Release":
81             log.msg("Registering package file: "+cache_path)
82             self.packages[cache_path] = file_path
83             return True
84         return False
85
86     def check_files(self):
87         """
88         Check all files in the database to make sure it exists.
89         """
90         files = self.packages.keys()
91         #print self.packages.keys()
92         for f in files:
93             if not os.path.exists(self.packages[f]):
94                 log.msg("File in packages database has been deleted: "+f)
95                 del self.packages[f]
96                 
97     def __getitem__(self, key): return self.packages[key]
98     def __setitem__(self, key, item): self.packages[key] = item
99     def __delitem__(self, key): del self.packages[key]
100     def keys(self): return self.packages.keys()
101
102 class AptPackages:
103     """
104     Uses AptPackagesServer to answer queries about packages.
105
106     Makes a fake configuration for python-apt for each backend.
107     """
108     DEFAULT_APT_CONFIG = {
109         #'APT' : '',
110         #'APT::Architecture' : 'amd64',  # TODO: Fix this, see bug #436011 and #285360
111         #'APT::Default-Release' : 'unstable',
112    
113         'Dir':'.', # /
114         'Dir::State' : 'apt/', # var/lib/apt/
115         'Dir::State::Lists': 'lists/', # lists/
116         #'Dir::State::cdroms' : 'cdroms.list',
117         'Dir::State::userstatus' : 'status.user',
118         'Dir::State::status': 'dpkg/status', # '/var/lib/dpkg/status'
119         'Dir::Cache' : '.apt/cache/', # var/cache/apt/
120         #'Dir::Cache::archives' : 'archives/',
121         'Dir::Cache::srcpkgcache' : 'srcpkgcache.bin',
122         'Dir::Cache::pkgcache' : 'pkgcache.bin',
123         'Dir::Etc' : 'apt/etc/', # etc/apt/
124         'Dir::Etc::sourcelist' : 'sources.list',
125         'Dir::Etc::vendorlist' : 'vendors.list',
126         'Dir::Etc::vendorparts' : 'vendors.list.d',
127         #'Dir::Etc::main' : 'apt.conf',
128         #'Dir::Etc::parts' : 'apt.conf.d',
129         #'Dir::Etc::preferences' : 'preferences',
130         'Dir::Bin' : '',
131         #'Dir::Bin::methods' : '', #'/usr/lib/apt/methods'
132         'Dir::Bin::dpkg' : '/usr/bin/dpkg',
133         #'DPkg' : '',
134         #'DPkg::Pre-Install-Pkgs' : '',
135         #'DPkg::Tools' : '',
136         #'DPkg::Tools::Options' : '',
137         #'DPkg::Tools::Options::/usr/bin/apt-listchanges' : '',
138         #'DPkg::Tools::Options::/usr/bin/apt-listchanges::Version' : '2',
139         #'DPkg::Post-Invoke' : '',
140         }
141     essential_dirs = ('apt', 'apt/cache', 'apt/dpkg', 'apt/etc', 'apt/lists',
142                       'apt/lists/partial')
143     essential_files = ('apt/dpkg/status', 'apt/etc/sources.list',)
144         
145     def __init__(self, backendName, cache_dir):
146         """
147         Construct new packages manager
148         backend: Name of backend associated with this packages file
149         cache_dir: cache directory from config file
150         """
151         self.backendName = backendName
152         self.cache_dir = cache_dir
153         self.apt_config = copy.deepcopy(self.DEFAULT_APT_CONFIG)
154
155         self.status_dir = (cache_dir+'/'+ aptpkg_dir
156                            +'/backends/'+backendName)
157         for dir in self.essential_dirs:
158             path = self.status_dir+'/'+dir
159             if not os.path.exists(path):
160                 os.makedirs(path)
161         for file in self.essential_files:
162             path = self.status_dir+'/'+file
163             if not os.path.exists(path):
164                 f = open(path,'w')
165                 f.close()
166                 del f
167                 
168         self.apt_config['Dir'] = self.status_dir
169         self.apt_config['Dir::State::status'] = self.status_dir + '/apt/dpkg/status'
170         #os.system('find '+self.status_dir+' -ls ')
171         #print "status:"+self.apt_config['Dir::State::status']
172         self.packages = PackageFileList(backendName, cache_dir)
173         self.loaded = 0
174         #print "Loaded aptPackages [%s] %s " % (self.backendName, self.cache_dir)
175         
176     def __del__(self):
177         self.cleanup()
178         #print "start aptPackages [%s] %s " % (self.backendName, self.cache_dir)
179         self.packages.close()
180         #print "Deleted aptPackages [%s] %s " % (self.backendName, self.cache_dir)
181     def file_updated(self, filename, cache_path, file_path):
182         """
183         A file in the backend has changed.  If this affects us, unload our apt database
184         """
185         if self.packages.update_file(filename, cache_path, file_path):
186             self.unload()
187
188     def __save_stdout(self):
189         self.real_stdout_fd = os.dup(1)
190         os.close(1)
191                 
192     def __restore_stdout(self):
193         os.dup2(self.real_stdout_fd, 1)
194         os.close(self.real_stdout_fd)
195         del self.real_stdout_fd
196
197     def load(self):
198         """
199         Regenerates the fake configuration and load the packages server.
200         """
201         if self.loaded: return True
202         apt_pkg.InitSystem()
203         #print "Load:", self.status_dir
204         shutil.rmtree(self.status_dir+'/apt/lists/')
205         os.makedirs(self.status_dir+'/apt/lists/partial')
206         sources_filename = self.status_dir+'/'+'apt/etc/sources.list'
207         sources = open(sources_filename, 'w')
208         sources_count = 0
209         self.packages.check_files()
210         for f in self.packages:
211             # we should probably clear old entries from self.packages and
212             # take into account the recorded mtime as optimization
213             filepath = self.packages[f]
214             fake_uri='http://apt-dht/'+f
215             source_line='deb '+dirname(fake_uri)+'/ /'
216             listpath=(self.status_dir+'/apt/lists/'
217                     +apt_pkg.URItoFileName(fake_uri))
218             sources.write(source_line+'\n')
219             log.msg("Sources line: " + source_line)
220             sources_count = sources_count + 1
221
222             try:
223                 #we should empty the directory instead
224                 os.unlink(listpath)
225             except:
226                 pass
227             os.symlink(self.packages[f], listpath)
228         sources.close()
229
230         if sources_count == 0:
231             log.msg("No Packages files available for %s backend"%(self.backendName))
232             return False
233
234         log.msg("Loading Packages database for "+self.status_dir)
235         #apt_pkg.Config = apt_pkg.newConfiguration(); #-- this causes unit tests to fail!
236         for key, value in self.apt_config.items():
237             apt_pkg.Config[key] = value
238 #         print "apt_pkg config:"
239 #         for I in apt_pkg.Config.keys():
240 #            print "%s \"%s\";"%(I,apt_pkg.Config[I]);
241
242         # apt_pkg prints progress messages to stdout, disable
243         self.__save_stdout()
244         try:
245             self.cache = apt_pkg.GetCache()
246         finally:
247             self.__restore_stdout()
248
249         self.records = apt_pkg.GetPkgRecords(self.cache)
250         #for p in self.cache.Packages:
251         #    print p
252         #log.debug("%s packages found" % (len(self.cache)),'apt_pkg')
253         self.loaded = 1
254         return True
255
256     def unload(self):
257         "Tries to make the packages server quit."
258         if self.loaded:
259             del self.cache
260             del self.records
261             self.loaded = 0
262
263     def cleanup(self):
264         self.unload()
265
266     def get_mirror_path(self, name, version):
267         "Find the path for version 'version' of package 'name'"
268         if not self.load(): return None
269         try:
270             for pack_vers in self.cache[name].VersionList:
271                 if(pack_vers.VerStr == version):
272                     file, index = pack_vers.FileList[0]
273                     self.records.Lookup((file,index))
274                     path = self.records.FileName
275                     if len(path)>2 and path[0:2] == './': 
276                         path = path[2:] # Remove any leading './'
277                     return path
278
279         except KeyError:
280             pass
281         return None
282       
283
284     def get_mirror_versions(self, package_name):
285         """
286         Find the available versions of the package name given
287         @type package_name: string
288         @param package_name: package name to search for e.g. ;apt'
289         @return: A list of mirror versions available
290
291         """
292         vers = []
293         if not self.load(): return vers
294         try:
295             for pack_vers in self.cache[package_name].VersionList:
296                 vers.append(pack_vers.VerStr)
297         except KeyError:
298             pass
299         return vers
300
301
302 def cleanup(factory):
303     for backend in factory.backends.values():
304         backend.get_packages_db().cleanup()
305
306 def get_mirror_path(factory, file):
307     """
308     Look for the path of 'file' in all backends.
309     """
310     info = AptDpkgInfo(file)
311     paths = []
312     for backend in factory.backends.values():
313         path = backend.get_packages_db().get_mirror_path(info['Package'],
314                                                 info['Version'])
315         if path:
316             paths.append('/'+backend.base+'/'+path)
317     return paths
318
319 def get_mirror_versions(factory, package):
320     """
321     Look for the available version of a package in all backends, given
322     an existing package name
323     """
324     all_vers = []
325     for backend in factory.backends.values():
326         vers = backend.get_packages_db().get_mirror_versions(package)
327         for ver in vers:
328             path = backend.get_packages_db().get_mirror_path(package, ver)
329             all_vers.append((ver, "%s/%s"%(backend.base,path)))
330     return all_vers
331
332 def closest_match(info, others):
333     def compare(a, b):
334         return apt_pkg.VersionCompare(a[0], b[0])
335
336     others.sort(compare)
337     version = info['Version']
338     match = None
339     for ver,path in others:
340         if version <= ver:
341             match = path
342             break
343     if not match:
344         if not others:
345             return None
346         match = others[-1][1]
347
348     dirname=re.sub(r'/[^/]*$', '', match)
349     version=re.sub(r'^[^:]*:', '', info['Version'])
350     if dirname.find('/pool/') != -1:
351         return "/%s/%s_%s_%s.deb"%(dirname, info['Package'],
352                                   version, info['Architecture'])
353     else:
354         return "/%s/%s_%s.deb"%(dirname, info['Package'], version)
355
356 def import_directory(factory, dir, recursive=0):
357     """
358     Import all files in a given directory into the cache
359     This is used by apt-proxy-import to import new files
360     into the cache
361     """
362     imported_count  = 0
363
364     if not os.path.exists(dir):
365         log.err('Directory ' + dir + ' does not exist')
366         return
367
368     if recursive:    
369         log.msg("Importing packages from directory tree: " + dir)
370         for root, dirs, files in os.walk(dir):
371             for file in files:
372                 imported_count += import_file(factory, root, file)
373     else:
374         log.msg("Importing packages from directory: " + dir)
375         for file in os.listdir(dir):
376             mode = os.stat(dir + '/' + file)[stat.ST_MODE]
377             if not stat.S_ISDIR(mode):
378                 imported_count += import_file(factory, dir, file)
379
380     for backend in factory.backends.values():
381         backend.get_packages_db().unload()
382
383     log.msg("Imported %s files" % (imported_count))
384     return imported_count
385
386 def import_file(factory, dir, file):
387     """
388     Import a .deb or .udeb into cache from given filename
389     """
390     if file[-4:]!='.deb' and file[-5:]!='.udeb':
391         log.msg("Ignoring (unknown file type):"+ file)
392         return 0
393     
394     log.msg("considering: " + dir + '/' + file)
395     try:
396         paths = get_mirror_path(factory, dir+'/'+file)
397     except SystemError:
398         log.msg(file + ' skipped - wrong format or corrupted')
399         return 0
400     if paths:
401         if len(paths) != 1:
402             log.msg("WARNING: multiple ocurrences")
403             log.msg(str(paths), 'import')
404         cache_path = paths[0]
405     else:
406         log.msg("Not found, trying to guess")
407         info = AptDpkgInfo(dir+'/'+file)
408         cache_path = closest_match(info,
409                                 get_mirror_versions(factory, info['Package']))
410     if cache_path:
411         log.msg("MIRROR_PATH:"+ cache_path)
412         src_path = dir+'/'+file
413         dest_path = factory.config.cache_dir+cache_path
414         
415         if not os.path.exists(dest_path):
416             log.msg("IMPORTING:" + src_path)
417             dest_path = re.sub(r'/\./', '/', dest_path)
418             if not os.path.exists(dirname(dest_path)):
419                 os.makedirs(dirname(dest_path))
420             f = open(dest_path, 'w')
421             fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
422             f.truncate(0)
423             shutil.copy2(src_path, dest_path)
424             f.close()
425             if hasattr(factory, 'access_times'):
426                 atime = os.stat(src_path)[stat.ST_ATIME]
427                 factory.access_times[cache_path] = atime
428             log.msg(file + ' imported')
429             return 1
430         else:
431             log.msg(file + ' skipped - already in cache')
432             return 0
433
434     else:
435         log.msg(file + ' skipped - no suitable backend found')
436         return 0
437             
438 class TestAptPackages(unittest.TestCase):
439     """Unit tests for the AptPackages cache."""
440     
441     pending_calls = []
442
443     def test_sha1(self):
444         a = AptPackages('whatever', '/tmp')
445     
446         packagesFile = os.popen('ls -Sr /var/lib/apt/lists/ | tail -n 1').read().rstrip('\n')
447         for f in os.walk('/var/lib/apt/lists').next()[2]:
448             if f[-7:] == "Release" and packagesFile.startswith(f[:-7]):
449                 releaseFile = f
450                 break
451         
452         a.file_updated('Release', releaseFile[releaseFile.find('_debian_')+1:].replace('_','/'), '/var/lib/apt/lists/' + releaseFile)
453         a.file_updated('Packages', packagesFile[packagesFile.find('_debian_')+1:].replace('_','/'), '/var/lib/apt/lists/' + packagesFile)
454     
455         a.load()
456     
457         a.records.Lookup(a.cache['dpkg'].VersionList[0].FileList[0])
458         
459         pkg_hash = os.popen('grep -A 30 -E "^Package: dpkg$" ' + '/var/lib/apt/lists/' + packagesFile + ' | grep -E "^SHA1:" | head -n 1 | cut -d\  -f 2').read().rstrip('\n')
460
461         self.failUnless(a.records.SHA1Hash == pkg_hash)
462
463     def tearDown(self):
464         for p in self.pending_calls:
465             if p.active():
466                 p.cancel()
467         self.pending_calls = []