summaryrefslogtreecommitdiffstats
path: root/pym/portage/news.py
blob: da7e159af2f593833ece45cf9a92987a196167e9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# portage: news management code
# Copyright 2006 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Id$

import errno
import os
import re
from portage.const import INCREMENTALS, PROFILE_PATH, NEWS_LIB_PATH
from portage.util import ensure_dirs, apply_permissions, normalize_path, grabfile, write_atomic
from portage.data import portage_gid
from portage.locks import lockfile, unlockfile, lockdir, unlockdir
from portage.exception import FileNotFound, OperationNotPermitted

class NewsManager(object):
	"""
	This object manages GLEP 42 style news items.  It will cache news items
	that have previously shown up and notify users when there are relevant news
	items that apply to their packages that the user has not previously read.
	
	Creating a news manager requires:
	root - typically ${ROOT} see man make.conf and man emerge for details
	news_path - path to news items; usually $REPODIR/metadata/news
	unread_path - path to the news.repoid.unread file; this helps us track news items
	
	"""

	def __init__(self, portdb, vardb, news_path, unread_path, language_id='en'):
		self.news_path = news_path
		self.unread_path = unread_path
		self.target_root = vardb.root
		self.language_id = language_id
		self.config = vardb.settings
		self.vdb = vardb
		self.portdb = portdb

		portdir = portdb.porttree_root
		profiles_base = os.path.join(portdir, 'profiles') + os.path.sep
		profile_path = None
		if portdb.mysettings.profile_path:
			profile_path = normalize_path(
				os.path.realpath(portdb.mysettings.profile_path))
			if profile_path.startswith(profiles_base):
				profile_path = profile_path[len(profiles_base):]
		self._profile_path = profile_path

		# Ensure that the unread path exists and is writable.
		dirmode  = 02070
		modemask =    02
		try:
			ensure_dirs(self.unread_path, mode=dirmode,
				mask=modemask, gid=portage_gid)
		except OperationNotPermitted:
			pass

	def updateItems(self, repoid):
		"""
		Figure out which news items from NEWS_PATH are both unread and relevant to
		the user (according to the GLEP 42 standards of relevancy).  Then add these
		items into the news.repoid.unread file.
		"""

		repos = self.portdb.getRepositories()
		if repoid not in repos:
			raise ValueError("Invalid repoID: %s" % repoid)

		path = os.path.join(self.portdb.getRepositoryPath(repoid), self.news_path)

		# Skip reading news for repoid if the news dir does not exist.  Requested by
		# NightMorph :)
		if not os.path.exists(path):
			return None
		news = os.listdir(path)

		skipfile = os.path.join(self.unread_path, "news-%s.skip" % repoid)
		skiplist = grabfile(skipfile)
		updates = []
		for itemid in news:
			if itemid in skiplist:
				continue
			try:
				filename = os.path.join(path, itemid, itemid + "." + self.language_id + ".txt")
				item = NewsItem(filename, itemid)
			except (TypeError):
				continue
			if item.isRelevant(profile=self._profile_path,
				config=self.config, vardb=self.vdb):
				updates.append(item)
		del path
		
		path = os.path.join(self.unread_path, 'news-%s.unread' % repoid)
		try:
			unread_lock = lockfile(path)
			if not os.path.exists(path):
				#create the file if it does not exist
				open(path, "w")
			# Ensure correct perms on the unread file.
			apply_permissions( filename=path,
				uid=int(self.config['PORTAGE_INST_UID']), gid=portage_gid, mode=0664)
			# Make sure we have the correct permissions when created
			unread_file = open(path, 'a')

			for item in updates:
				unread_file.write(item.name + "\n")
				skiplist.append(item.name)
			unread_file.close()
		finally:
			unlockfile(unread_lock)
			write_atomic(skipfile, "\n".join(skiplist)+"\n")
		try:
			apply_permissions(filename=skipfile, 
				uid=int(self.config["PORTAGE_INST_UID"]), gid=portage_gid, mode=0664)
		except OperationNotPermitted, e:
			import errno
			# skip "permission denied" errors as we're likely running in pretend mode
			# with reduced priviledges
			if e.errno == errno.EPERM:
				pass
			else:
				raise

	def getUnreadItems(self, repoid, update=False):
		"""
		Determine if there are unread relevant items in news.repoid.unread.
		If there are unread items return their number.
		If update is specified, updateNewsItems( repoid ) will be called to
		check for new items.
		"""
		
		if update:
			self.updateItems(repoid)
		
		unreadfile = os.path.join(self.unread_path, 'news-%s.unread' % repoid)
		unread_lock = None
		try:
			if os.access(os.path.dirname(unreadfile), os.W_OK):
				# TODO: implement shared readonly locks
				unread_lock = lockfile(unreadfile)
			try:
				f = open(unreadfile)
				try:
					unread = f.readlines()
				finally:
					f.close()
			except EnvironmentError, e:
				if e.errno != errno.ENOENT:
					raise
				del e
				return 0
			if len(unread):
				return len(unread)
		finally:
			if unread_lock:
				unlockfile(unread_lock)

_installedRE = re.compile("Display-If-Installed:(.*)\n")
_profileRE = re.compile("Display-If-Profile:(.*)\n")
_keywordRE = re.compile("Display-If-Keyword:(.*)\n")

class NewsItem(object):
	"""
	This class encapsulates a GLEP 42 style news item.
	It's purpose is to wrap parsing of these news items such that portage can determine
	whether a particular item is 'relevant' or not.  This requires parsing the item
	and determining 'relevancy restrictions'; these include "Display if Installed" or
	"display if arch: x86" and so forth.

	Creation of a news item involves passing in the path to the particular news item.

	"""
	
	def __init__(self, path, name):
		""" 
		For a given news item we only want if it path is a file.
		"""
		if not os.path.isfile(path):
			raise TypeError("%s is no regular file" % path)
		self.path = path
		self.name = name
		self._parsed = False

	def isRelevant(self, vardb, config, profile):
		"""
		This function takes a dict of keyword arguments; one should pass in any
		objects need to do to lookups (like what keywords we are on, what profile,
		and a vardb so we can look at installed packages).
		Each restriction will pluck out the items that are required for it to match
		or raise a ValueError exception if the required object is not present.
		"""

		if not len(self.restrictions):
			return True # no restrictions to match means everyone should see it
		
		kwargs = \
			{ 'vardb' : vardb,
				'config' : config,
				'profile' : profile }

		for restriction in self.restrictions:
			if restriction.checkRestriction(**kwargs):
				return True
			
		return False # No restrictions were met; thus we aren't relevant :(

	def parse(self):
		lines = open(self.path).readlines()
		self.restrictions = []
		for line in lines:
			#Optimization to ignore regex matchines on lines that
			#will never match
			if not line.startswith('D'):
				continue
			restricts = {  _installedRE : DisplayInstalledRestriction,
					_profileRE : DisplayProfileRestriction,
					_keywordRE : DisplayKeywordRestriction }
			for regex, restriction in restricts.iteritems():
				match = regex.match(line)
				if match:
					self.restrictions.append(restriction(match.groups()[0].strip()))
					continue
		self._parsed = True

	def __getattr__(self, attr):
		if not self._parsed:
			self.parse()
		return self.__dict__[attr]

class DisplayRestriction(object):
	"""
	A base restriction object representing a restriction of display.
	news items may have 'relevancy restrictions' preventing them from
	being important.  In this case we need a manner of figuring out if
	a particular item is relevant or not.  If any of it's restrictions
	are met, then it is displayed
	"""

	def checkRestriction(self, **kwargs):
		raise NotImplementedError('Derived class should over-ride this method')

class DisplayProfileRestriction(DisplayRestriction):
	"""
	A profile restriction where a particular item shall only be displayed
	if the user is running a specific profile.
	"""

	def __init__(self, profile):
		self.profile = profile

	def checkRestriction(self, **kwargs):
		if self.profile == kwargs['profile']:
			return True
		return False

class DisplayKeywordRestriction(DisplayRestriction):
	"""
	A keyword restriction where a particular item shall only be displayed
	if the user is running a specific keyword.
	"""

	def __init__(self, keyword):
		self.keyword = keyword

	def checkRestriction(self, **kwargs):
		if kwargs['config']['ARCH'] == self.keyword:
			return True
		return False

class DisplayInstalledRestriction(DisplayRestriction):
	"""
	An Installation restriction where a particular item shall only be displayed
	if the user has that item installed.
	"""
	
	def __init__(self, cpv):
		self.cpv = cpv

	def checkRestriction(self, **kwargs):
		vdb = kwargs['vardb']
		if vdb.match(self.cpv):
			return True
		return False