~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/logfile/__init__.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mfrom: (0.9.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080622211713-fpo2zrq3s5dfecxg
Tags: 1.7.0-3
Simplify /etc/moin/wikilist format: "USER URL" (drop unneeded middle
CONFIG_DIR that was wrongly advertised as DATA_DIR).  Make
moin-mass-migrate handle both formats and warn about deprecation of
the old one.

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
"""
3
3
    MoinMoin - LogFile package
4
4
 
5
 
    @copyright: 2005 by Thomas Waldmann (MoinMoin:ThomasWaldmann)
 
5
    This module supports buffered log reads, iterating forward and backward line-by-line, etc.
 
6
 
 
7
    @copyright: 2005-2007 MoinMoin:ThomasWaldmann
6
8
    @license: GNU GPL, see COPYING for details.
7
9
"""
8
10
 
9
 
from MoinMoin.util import pysupport
10
 
 
11
 
logfiles = pysupport.getPackageModules(__file__)
12
 
 
 
11
from MoinMoin import log
 
12
logging = log.getLogger(__name__)
 
13
 
 
14
import os, codecs, errno
 
15
from MoinMoin import config, wikiutil
 
16
 
 
17
class LogError(Exception):
 
18
    """ Base class for log errors """
 
19
 
 
20
class LogMissing(LogError):
 
21
    """ Raised when the log is missing """
 
22
 
 
23
 
 
24
class LineBuffer:
 
25
    """
 
26
    Reads lines from a file
 
27
        self.len      number of lines in self.lines
 
28
        self.lines    list of lines (unicode)
 
29
        self.offsets  list of file offsets for each line. additionally the position
 
30
                      after the last read line is stored into self.offsets[-1]
 
31
    """
 
32
    def __init__(self, file, offset, size, forward=True):
 
33
        """
 
34
 
 
35
        TODO: when this gets refactored, don't use "file" (is a builtin)
 
36
 
 
37
        @param file: open file object
 
38
        @param offset: position in file to start from
 
39
        @param size: aproximate number of bytes to read
 
40
        @param forward : read from offset on or from offset-size to offset
 
41
        @type forward: boolean
 
42
        """
 
43
        self.loglevel = logging.NOTSET
 
44
        if forward:
 
45
            begin = offset
 
46
            logging.log(self.loglevel, "LineBuffer.init: forward seek %d read %d" % (begin, size))
 
47
            file.seek(begin)
 
48
            lines = file.readlines(size)
 
49
        else:
 
50
            if offset < 2 * size:
 
51
                begin = 0
 
52
                size = offset
 
53
            else:
 
54
                begin = offset - size
 
55
            logging.log(self.loglevel, "LineBuffer.init: backward seek %d read %d" % (begin, size))
 
56
            file.seek(begin)
 
57
            lines = file.read(size).splitlines(True)
 
58
            if begin != 0:
 
59
                # remove potentially incomplete first line
 
60
                begin += len(lines[0])
 
61
                lines = lines[1:]
 
62
                # XXX check for min one line read
 
63
 
 
64
        linecount = len(lines)
 
65
 
 
66
        # now calculate the file offsets of all read lines
 
67
        offsets = [len(line) for line in lines]
 
68
        offsets.append(0) # later this element will have the file offset after the last read line
 
69
 
 
70
        lengthpreviousline = 0
 
71
        offset = begin
 
72
        for i in xrange(linecount+1):
 
73
            offset += lengthpreviousline
 
74
            lengthpreviousline = offsets[i]
 
75
            offsets[i] = offset
 
76
 
 
77
        self.offsets = offsets
 
78
        self.len = linecount
 
79
        # Decode lines after offset in file is calculated
 
80
        self.lines = [unicode(line, config.charset) for line in lines]
 
81
 
 
82
 
 
83
class LogFile:
 
84
    """
 
85
    .filter: function that gets the values from .parser.
 
86
             must return True to keep it or False to remove it
 
87
    Overwrite .parser() and .add() to customize this class to special log files
 
88
    """
 
89
 
 
90
    def __init__(self, filename, buffer_size=4096):
 
91
        """
 
92
        @param filename: name of the log file
 
93
        @param buffer_size: approx. size of one buffer in bytes
 
94
        """
 
95
        self.loglevel = logging.NOTSET
 
96
        self.__filename = filename
 
97
        self.__buffer = None # currently used buffer, points to one of the following:
 
98
        self.__buffer1 = None
 
99
        self.__buffer2 = None
 
100
        self.buffer_size = buffer_size
 
101
        self.__lineno = 0
 
102
        self.filter = None
 
103
 
 
104
    def __iter__(self):
 
105
        return self
 
106
 
 
107
    def reverse(self):
 
108
        """ yield log entries in reverse direction starting from last one
 
109
 
 
110
        @rtype: iterator
 
111
        """
 
112
        self.to_end()
 
113
        while 1:
 
114
            try:
 
115
                logging.log(self.loglevel, "LogFile.reverse %s" % self.__filename)
 
116
                result = self.previous()
 
117
            except StopIteration:
 
118
                return
 
119
            yield result
 
120
 
 
121
    def sanityCheck(self):
 
122
        """ Check for log file write access.
 
123
 
 
124
        @rtype: string (error message) or None
 
125
        """
 
126
        if not os.access(self.__filename, os.W_OK):
 
127
            return "The log '%s' is not writable!" % (self.__filename, )
 
128
        return None
 
129
 
 
130
    def __getattr__(self, name):
 
131
        """
 
132
        generate some attributes when needed
 
133
        """
 
134
        if name == "_LogFile__rel_index": # Python black magic: this is the real name of the __rel_index attribute
 
135
            # starting iteration from begin
 
136
            self.__buffer1 = LineBuffer(self._input, 0, self.buffer_size)
 
137
            self.__buffer2 = LineBuffer(self._input,
 
138
                                        self.__buffer1.offsets[-1],
 
139
                                        self.buffer_size)
 
140
            self.__buffer = self.__buffer1
 
141
            self.__rel_index = 0
 
142
            return 0
 
143
        elif name == "_input":
 
144
            try:
 
145
                # Open the file (NOT using codecs.open, it breaks our offset calculation. We decode it later.).
 
146
                # Use binary mode in order to retain \r - otherwise the offset calculation would fail.
 
147
                self._input = file(self.__filename, "rb", )
 
148
            except IOError, err:
 
149
                if err.errno == errno.ENOENT: # "file not found"
 
150
                    # XXX workaround if edit-log does not exist: just create it empty
 
151
                    # if this workaround raises another error, we don't catch
 
152
                    # it, so the admin will see it.
 
153
                    f = file(self.__filename, "ab")
 
154
                    f.write('')
 
155
                    f.close()
 
156
                    self._input = file(self.__filename, "rb", )
 
157
                else:
 
158
                    logging.error("logfile: %r IOERROR errno %d (%s)" % (self.__filename, err.errno, os.strerror(err.errno)))
 
159
                    raise
 
160
            return self._input
 
161
        elif name == "_output":
 
162
            self._output = codecs.open(self.__filename, 'a', config.charset)
 
163
            return self._output
 
164
        else:
 
165
            raise AttributeError(name)
 
166
 
 
167
    def size(self):
 
168
        """ Return log size in bytes
 
169
 
 
170
        Return 0 if the file does not exist. Raises other OSError.
 
171
 
 
172
        @return: size of log file in bytes
 
173
        @rtype: Int
 
174
        """
 
175
        try:
 
176
            return os.path.getsize(self.__filename)
 
177
        except OSError, err:
 
178
            if err.errno == errno.ENOENT:
 
179
                return 0
 
180
            raise
 
181
 
 
182
    def lines(self):
 
183
        """ Return number of lines in the log file
 
184
 
 
185
        Return 0 if the file does not exist. Raises other OSError.
 
186
 
 
187
        Expensive for big log files - O(n)
 
188
 
 
189
        @return: size of log file in lines
 
190
        @rtype: Int
 
191
        """
 
192
        try:
 
193
            f = file(self.__filename, 'r')
 
194
            try:
 
195
                count = 0
 
196
                for line in f:
 
197
                    count += 1
 
198
                return count
 
199
            finally:
 
200
                f.close()
 
201
        except (OSError, IOError), err:
 
202
            if err.errno == errno.ENOENT:
 
203
                return 0
 
204
            raise
 
205
 
 
206
    def date(self):
 
207
        # ToDo check if we need this method
 
208
        """ Return timestamp of log file in usecs """
 
209
        try:
 
210
            mtime = os.path.getmtime(self.__filename)
 
211
        except OSError, err:
 
212
            if err.errno == errno.ENOENT:
 
213
                # This can happen on fresh wiki when building the index
 
214
                # Usually the first request will create an event log
 
215
                raise LogMissing(str(err))
 
216
            raise
 
217
        return wikiutil.timestamp2version(mtime)
 
218
 
 
219
    def peek(self, lines):
 
220
        """ Move position in file forward or backwards by "lines" count
 
221
 
 
222
        It adjusts .__lineno if set.
 
223
        This function is not aware of filters!
 
224
 
 
225
        @param lines: number of lines, may be negative to move backward
 
226
        @rtype: boolean
 
227
        @return: True if moving more than to the beginning and moving
 
228
                 to the end or beyond
 
229
        """
 
230
        logging.log(self.loglevel, "LogFile.peek %s" % self.__filename)
 
231
        self.__rel_index += lines
 
232
        while self.__rel_index < 0:
 
233
            if self.__buffer is self.__buffer2:
 
234
                if self.__buffer.offsets[0] == 0:
 
235
                    # already at the beginning of the file
 
236
                    self.__rel_index = 0
 
237
                    self.__lineno = 0
 
238
                    return True
 
239
                else:
 
240
                    # change to buffer 1
 
241
                    self.__buffer = self.__buffer1
 
242
                    self.__rel_index += self.__buffer.len
 
243
            else: # self.__buffer is self.__buffer1
 
244
                if self.__buffer.offsets[0] == 0:
 
245
                    # already at the beginning of the file
 
246
                    self.__rel_index = 0
 
247
                    self.__lineno = 0
 
248
                    return True
 
249
                else:
 
250
                    # load previous lines
 
251
                    self.__buffer2 = self.__buffer1
 
252
                    self.__buffer1 = LineBuffer(self._input,
 
253
                                                self.__buffer.offsets[0],
 
254
                                                self.buffer_size,
 
255
                                                forward=False)
 
256
                    self.__buffer = self.__buffer1
 
257
                    self.__rel_index += self.__buffer.len
 
258
 
 
259
        while self.__rel_index >= self.__buffer.len:
 
260
            if self.__buffer is self.__buffer1:
 
261
                # change to buffer 2
 
262
                self.__rel_index -= self.__buffer.len
 
263
                self.__buffer = self.__buffer2
 
264
            else: # self.__buffer is self.__buffer2
 
265
                # try to load next buffer
 
266
                tmpbuff = LineBuffer(self._input,
 
267
                                     self.__buffer.offsets[-1],
 
268
                                     self.buffer_size)
 
269
                if tmpbuff.len == 0:
 
270
                    # end of file
 
271
                    if self.__lineno is not None:
 
272
                        self.__lineno += (lines -
 
273
                                         (self.__rel_index - self.__buffer.len))
 
274
                    self.__rel_index = self.__buffer.len # point to after last read line
 
275
                    return True
 
276
                # shift buffers
 
277
                self.__rel_index -= self.__buffer.len
 
278
                self.__buffer1 = self.__buffer2
 
279
                self.__buffer2 = tmpbuff
 
280
                self.__buffer = self.__buffer2
 
281
 
 
282
        if self.__lineno is not None:
 
283
            self.__lineno += lines
 
284
        return False
 
285
 
 
286
    def __next(self):
 
287
        """get next line already parsed"""
 
288
        if self.peek(0):
 
289
            raise StopIteration
 
290
        result = self.parser(self.__buffer.lines[self.__rel_index])
 
291
        self.peek(1)
 
292
        return result
 
293
 
 
294
    def next(self):
 
295
        """get next line that passes through the filter
 
296
        @return: next entry
 
297
        raises StopIteration at file end
 
298
        """
 
299
        result = None
 
300
        while result is None:
 
301
            while result is None:
 
302
                logging.log(self.loglevel, "LogFile.next %s" % self.__filename)
 
303
                result = self.__next()
 
304
            if self.filter and not self.filter(result):
 
305
                result = None
 
306
        return result
 
307
 
 
308
    def __previous(self):
 
309
        """get previous line already parsed"""
 
310
        if self.peek(-1):
 
311
            raise StopIteration
 
312
        return self.parser(self.__buffer.lines[self.__rel_index])
 
313
 
 
314
    def previous(self):
 
315
        """get previous line that passes through the filter
 
316
        @return: previous entry
 
317
        raises StopIteration at file begin
 
318
        """
 
319
        result = None
 
320
        while result is None:
 
321
            while result is None:
 
322
                logging.log(self.loglevel, "LogFile.previous %s" % self.__filename)
 
323
                result = self.__previous()
 
324
            if self.filter and not self.filter(result):
 
325
                result = None
 
326
        return result
 
327
 
 
328
    def to_begin(self):
 
329
        """moves file position to the begin"""
 
330
        logging.log(self.loglevel, "LogFile.to_begin %s" % self.__filename)
 
331
        if self.__buffer1 is None or self.__buffer1.offsets[0] != 0:
 
332
            self.__buffer1 = LineBuffer(self._input,
 
333
                                        0,
 
334
                                        self.buffer_size)
 
335
            self.__buffer2 = LineBuffer(self._input,
 
336
                                        self.__buffer1.offsets[-1],
 
337
                                        self.buffer_size)
 
338
        self.__buffer = self.__buffer1
 
339
        self.__rel_index = 0
 
340
        self.__lineno = 0
 
341
 
 
342
    def to_end(self):
 
343
        """moves file position to the end"""
 
344
        logging.log(self.loglevel, "LogFile.to_end %s" % self.__filename)
 
345
        self._input.seek(0, 2) # to end of file
 
346
        size = self._input.tell()
 
347
        if self.__buffer2 is None or size > self.__buffer2.offsets[-1]:
 
348
            self.__buffer2 = LineBuffer(self._input,
 
349
                                        size,
 
350
                                        self.buffer_size,
 
351
                                        forward=False)
 
352
 
 
353
            self.__buffer1 = LineBuffer(self._input,
 
354
                                        self.__buffer2.offsets[0],
 
355
                                        self.buffer_size,
 
356
                                        forward=False)
 
357
        self.__buffer = self.__buffer2
 
358
        self.__rel_index = self.__buffer2.len
 
359
        self.__lineno = None
 
360
 
 
361
    def position(self):
 
362
        """ Return the current file position
 
363
 
 
364
        This can be converted into a String using back-ticks and then be rebuild.
 
365
        For this plain file implementation position is an Integer.
 
366
        """
 
367
        return self.__buffer.offsets[self.__rel_index]
 
368
 
 
369
    def seek(self, position, line_no=None):
 
370
        """ moves file position to an value formerly gotten from .position().
 
371
        To enable line counting line_no must be provided.
 
372
        .seek is much more efficient for moving long distances than .peek.
 
373
        raises ValueError if position is invalid
 
374
        """
 
375
        logging.log(self.loglevel, "LogFile.seek %s pos %d" % (self.__filename, position))
 
376
        if self.__buffer1:
 
377
            logging.log(self.loglevel, "b1 %r %r" % (self.__buffer1.offsets[0], self.__buffer1.offsets[-1]))
 
378
        if self.__buffer2:
 
379
            logging.log(self.loglevel, "b2 %r %r" % (self.__buffer2.offsets[0], self.__buffer2.offsets[-1]))
 
380
        if self.__buffer1 and self.__buffer1.offsets[0] <= position < self.__buffer1.offsets[-1]:
 
381
            # position is in .__buffer1
 
382
            self.__rel_index = self.__buffer1.offsets.index(position)
 
383
            self.__buffer = self.__buffer1
 
384
        elif self.__buffer2 and self.__buffer2.offsets[0] <= position < self.__buffer2.offsets[-1]:
 
385
            # position is in .__buffer2
 
386
            self.__rel_index = self.__buffer2.offsets.index(position)
 
387
            self.__buffer = self.__buffer2
 
388
        elif self.__buffer1 and self.__buffer1.offsets[-1] == position:
 
389
            # we already have one buffer directly before where we want to go
 
390
            self.__buffer2 = LineBuffer(self._input,
 
391
                                        position,
 
392
                                        self.buffer_size)
 
393
            self.__buffer = self.__buffer2
 
394
            self.__rel_index = 0
 
395
        elif self.__buffer2 and self.__buffer2.offsets[-1] == position:
 
396
            # we already have one buffer directly before where we want to go
 
397
            self.__buffer1 = self.__buffer2
 
398
            self.__buffer2 = LineBuffer(self._input,
 
399
                                        position,
 
400
                                        self.buffer_size)
 
401
            self.__buffer = self.__buffer2
 
402
            self.__rel_index = 0
 
403
        else:
 
404
            # load buffers around position
 
405
            self.__buffer1 = LineBuffer(self._input,
 
406
                                        position,
 
407
                                        self.buffer_size,
 
408
                                        forward=False)
 
409
            self.__buffer2 = LineBuffer(self._input,
 
410
                                        position,
 
411
                                        self.buffer_size)
 
412
            self.__buffer = self.__buffer2
 
413
            self.__rel_index = 0
 
414
            # XXX test for valid position
 
415
        self.__lineno = line_no
 
416
 
 
417
    def line_no(self):
 
418
        """@return: the current line number or None if line number is unknown"""
 
419
        return self.__lineno
 
420
 
 
421
    def calculate_line_no(self):
 
422
        """ Calculate the current line number from buffer offsets
 
423
 
 
424
        If line number is unknown it is calculated by parsing the whole file.
 
425
        This may be expensive.
 
426
        """
 
427
        self._input.seek(0, 0)
 
428
        lines = self._input.read(self.__buffer.offsets[self.__rel_index])
 
429
        self.__lineno = len(lines.splitlines())
 
430
        return self.__lineno
 
431
 
 
432
    def parser(self, line):
 
433
        """
 
434
        @param line: line as read from file
 
435
        @return: parsed line or None on error
 
436
        Converts the line from file to program representation
 
437
        This implementation uses TAB separated strings.
 
438
        This method should be overwritten by the sub classes.
 
439
        """
 
440
        return line.split("\t")
 
441
 
 
442
    def add(self, *data):
 
443
        """
 
444
        add line to log file
 
445
        This implementation save the values as TAB separated strings.
 
446
        This method should be overwritten by the sub classes.
 
447
        """
 
448
        line = "\t".join(data)
 
449
        self._add(line)
 
450
 
 
451
    def _add(self, line):
 
452
        """
 
453
        @param line: flat line
 
454
        @type line: String
 
455
        write on entry in the log file
 
456
        """
 
457
        if line is not None:
 
458
            if line[-1] != '\n':
 
459
                line += '\n'
 
460
            self._output.write(line)
 
461
            self._output.close() # does this maybe help against the sporadic fedora wikis 160 \0 bytes in the edit-log?
 
462
            del self._output # re-open the output file automagically