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