omap3-pandora-kernel: bump for memhole stuff
[openpandora.oe.git] / recipes / pandora-system / pandora-pndstore / packages.py
1 """
2 This module implements a means of interacting with and acting on package
3 data.  Notable is the Package class that encapsulates all available versions of
4 a package, also allowing for installation and removal.  Also, the get_updates
5 function is useful.
6 """
7
8 import options, database_update, sqlite3, os, shutil, urllib2, glob
9 from hashlib import md5
10 from distutils.version import LooseVersion
11 from weakref import WeakValueDictionary
12 from database_update import LOCAL_TABLE, REPO_INDEX_TABLE, SEPCHAR
13
14
15 class PackageError(Exception): pass
16
17
18
19 class PNDVersion(LooseVersion):
20     """Gives the flexibility of distutils.version.LooseVersion, but ensures that
21     any text is always considered less than anything else (including nothing)."""
22     def __cmp__(self, other):
23         if isinstance(other, str):
24             other = self.__class__(other)
25
26         for i,j in map(None, self.version, other.version):
27             iStr = isinstance(i, basestring)
28             jStr = isinstance(j, basestring)
29             if iStr and not jStr: return -1
30             elif jStr and not iStr: return 1
31             else:
32                 c = cmp(i,j)
33                 if c != 0: return c
34         return 0 # If it hasn't returned yet, they're the same.
35
36
37
38 def get_remote_tables():
39     """Checks the remote index table to find the names of all tables containing
40     data from remote databases.  Returns a list of strings."""
41     with sqlite3.connect(options.get_database()) as db:
42         c = db.execute('Select url From "%s"' % REPO_INDEX_TABLE)
43         return [ i[0] for i in c ]
44
45
46 def get_searchpath_full():
47     paths = []
48     for p in options.get_searchpath():
49         paths.extend(glob.iglob(p))
50     return paths
51
52
53
54 class PackageInstance(object):
55     """Gives information on a package as available from a specific source.
56     This should not generally used by external applications.  The Package class
57     should cover all needs."""
58
59     def __init__(self, sourceid, pkgid):
60         "sourceid should be the name of the table in which to look for this package."
61         self.sourceid = sourceid
62         self.pkgid = pkgid
63
64         with sqlite3.connect(options.get_database()) as db:
65             db.row_factory = sqlite3.Row
66             # Will set db_entry to None if entry or table doesn't exist.
67             try:
68                 self.db_entry = db.execute('Select * From "%s" Where id=?'
69                     % database_update.sanitize_sql(sourceid), (pkgid,)).fetchone()
70             except sqlite3.OperationalError:
71                 self.db_entry = None
72
73         self.exists = self.db_entry is not None
74         self.version = PNDVersion(self.db_entry['version'] if self.exists
75             else 'A') # This should be the lowest possible version.
76
77
78     def install(self, installdir):
79         # Check if this is actually a locally installed file already.
80         if os.path.exists(self.db_entry['uri']):
81             raise PackageError('Package is already installed.')
82             # Or maybe skip the rest of the function without erroring.
83
84         # Make connection and determine filename.
85         p = urllib2.urlopen(self.db_entry['uri'])
86         header = p.info().getheader('content-disposition')
87         fkey = 'filename="'
88         if header and (fkey in header):
89             n = header.find(fkey) + len(fkey)
90             filename = header[n:].split('"')[0]
91         else:
92             filename = os.path.basename(p.geturl())
93         path = os.path.join(installdir, filename)
94
95         # Put file in place.  No need to check if it already exists; if it
96         # does, we probably want to replace it anyways.
97         # In the process, check its MD5 sum against the one given in the repo.
98         # MD5 is optional in the spec, so only calculate it if it's not given.
99         m_target = self.db_entry['md5']
100         if m_target: m = md5()
101         with open(path, 'wb') as dest:
102             for chunk in iter(lambda: p.read(128*m.block_size), ''):
103                 if m_target: m.update(chunk)
104                 dest.write(chunk)
105         if m_target and (m.hexdigest() != m_target):
106             raise PackageError("File corrupted.  MD5 sums do not match.")
107
108         # Update local database with new info.
109         with sqlite3.connect(options.get_database()) as db:
110             database_update.update_local_file(path, db)
111             db.commit()
112
113
114
115 class Package(object):
116     """Informs on and modifies any package defined by a package id.  Includes
117     all locally-installed and remotely-available versions of that package."""
118
119     # This ensures that only one Package object can exist for a given ID (a
120     # variant of the Singleton pattern?).  This lets Package objects be
121     # considered the same even if created independently of each other (such as
122     # through multiple calls to search_local_packages).  Also ensures that one
123     # instance installing or upgrading will not cause another instance to
124     # become out-of-date.
125     _existing = WeakValueDictionary()
126     def __new__(cls, pkgid):
127         try:
128             return cls._existing[pkgid]
129         except KeyError:
130             p = object.__new__(cls)
131             cls._existing[pkgid] = p
132             return p
133
134     def __init__(self, pkgid):
135         self.id = pkgid
136
137         self.local = PackageInstance(LOCAL_TABLE, pkgid)
138         self.remote = [PackageInstance(i, pkgid) for i in get_remote_tables()]
139
140
141     def get_latest_remote(self):
142         return max(self.remote, key=lambda x: x.version)
143
144
145     def get_latest(self):
146         """Returns PackageInstance of the most recent available version.
147         Gives preference to locally installed version."""
148         m = self.get_latest_remote()
149         return self.local.version >= m.version and self.local or m
150
151
152     def install(self, installdir):
153         """Installs the latest available version of the package to installdir.
154         Fails if package is already installed (which would create conflict in
155         libpnd) or if installdir is not on the searchpath (which would confuse
156         the database."""
157         # TODO: Repository selection (not just the most up-to-date one).
158
159         installdir = os.path.abspath(installdir)
160
161         if self.local.exists:
162             raise PackageError("Locally installed version of %s already exists.  Use upgrade method to reinstall." % self.id)
163
164         elif not os.path.isdir(installdir):
165             raise PackageError("%s is not a directory." % installdir)
166
167         elif installdir not in get_searchpath_full():
168             raise PackageError("Cannot install to %s since it's not on the searchpath."
169                 % installdir)
170
171         # Install the latest remote.
172         m = self.get_latest_remote()
173         if not m.exists:
174             raise PackageError('No remote from which to install %s.' % self.id)
175         m.install(installdir)
176         # Local table has changed, so update the local PackageInstance.
177         self.local = PackageInstance(LOCAL_TABLE, self.id)
178
179
180     def upgrade(self):
181         oldname = self.local.db_entry['uri']
182         installdir = os.path.dirname(oldname)
183
184         # Move old version to a temporary filename (that doesn't yet exist).
185         newname = oldname + '.temp'
186         while os.path.exists(newname):
187             newname += '.temp'
188         shutil.move(oldname, newname)
189
190         try:
191             # Install the latest remote.
192             m = self.get_latest_remote()
193             if not m.exists:
194                 raise PackageError('No remote from which to upgrade %s.' % self.id)
195             m.install(installdir)
196         except Exception as e:
197             # If upgrade fails, move old version back to where it was.
198             shutil.move(newname, oldname)
199             raise e
200         else:
201             # If upgrade succeeds, get rid of old version.
202             os.remove(newname)
203             # Local table has changed, so update the local PackageInstance.
204             self.local = PackageInstance(LOCAL_TABLE, self.id)
205
206
207     def remove(self):
208         "Remove any locally-installed copy of this package."
209         # Check if it's even locally installed.
210         if not self.local.exists:
211             raise PackageError("%s can't be removed since it's not installed." % self.id)
212         # If so, remove it.
213         os.remove(self.local.db_entry['uri'])
214         # Remove it from the local database.
215         with sqlite3.connect(options.get_database()) as db:
216             db.execute('Delete From "%s" Where id=?' % LOCAL_TABLE, (self.id,))
217             db.commit()
218         # Local table has changed, so update the local PackageInstance.
219         self.local = PackageInstance(LOCAL_TABLE, self.id)
220
221
222     def remove_appdatas(self):
223         # Use libpnd to find location of all appdatas.
224         # shutil.rmtree all of them
225         # Maybe create a way to remove appdatas of individual apps?
226         pass
227
228
229
230 def search_local_packages(col, val):
231     """Find all packages containing the given value in the given column.
232     Also handles columns containing lists of data, ensuring that the given
233     value is an entry of that list, not just a substring of an entry."""
234     with sqlite3.connect(options.get_database()) as db:
235         c = db.execute( '''Select id From "%(tab)s" Where %(col)s Like ?
236             Or %(col)s Like ? Or %(col)s Like ? Or %(col)s Like ?'''
237             % {'tab':LOCAL_TABLE, 'col':col},
238             (val, val+SEPCHAR+'%', '%'+SEPCHAR+val,
239             '%'+SEPCHAR+val+SEPCHAR+'%') )
240
241     return [ Package(i[0]) for i in c ]
242
243
244 def get_all():
245     "Returns Package object for every available package, local or remote."
246     tables = get_remote_tables()
247     tables.append(LOCAL_TABLE)
248     with sqlite3.connect(options.get_database()) as db:
249         c = db.execute(
250             ' Union '.join([ 'Select id From "%s"'%t for t in tables ]) )
251         return [ Package(i[0]) for i in c ]
252
253
254 def get_all_local():
255     """Returns Package object for every installed package."""
256     with sqlite3.connect(options.get_database()) as db:
257         c = db.execute('Select id From "%s"' % LOCAL_TABLE)
258         return [ Package(i[0]) for i in c ]
259
260
261 def get_updates():
262     """Checks for updates for all installed packages.
263     Returns a list of Package objects for which a remote version is newer than
264     the installed version.  Does not include packages that are not locally installed."""
265     # TODO: Possibly optimize by using SQL Joins to only check packages that
266     # are both locally and remotely available.
267     return [ i for i in get_all_local() if i.local is not i.get_latest() ]