~ubuntu-branches/ubuntu/gutsy/moin/gutsy

« back to all changes in this revision

Viewing changes to MoinMoin/lupy.py

  • Committer: Bazaar Package Importer
  • Author(s): Sivan Greenberg
  • Date: 2006-07-09 19:28:02 UTC
  • Revision ID: james.westby@ubuntu.com-20060709192802-oaeuvt4v3e9300uj
Tags: 1.5.3-1ubuntu1
* Merge new debian version.
* Reapply Ubuntu changes:
    + debian/rules:
      - Comment out usage of control.ubuntu.in (doesn't fit!).
    + debian/control.in:
      - Dropped python2.3 binary package.
    + debian/control:
      - Dropped python2.3 binary, again.
      - Dropped python2.3-dev from Build-Depends-Indep.
    + debian/patches/001-attachment-xss-fix.patch:
      - Dropped this patch. It's now in upstream's distribution.

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
"""
3
3
    MoinMoin - lupy indexing search engine
4
4
 
5
 
    @copyright: 2005 by Florian Festi, Nir Soffer
 
5
    @copyright: 2005 by Florian Festi, Nir Soffer, Thomas Waldmann
6
6
    @license: GNU GPL, see COPYING for details.
7
7
"""
8
8
 
9
9
import os, re, codecs, errno, time
10
10
 
11
11
from MoinMoin.Page import Page
12
 
from MoinMoin import config
 
12
from MoinMoin import config, wikiutil
13
13
from MoinMoin.util import filesys, lock
14
14
from MoinMoin.support.lupy.index.term import Term
15
15
from MoinMoin.support.lupy import document
16
16
from MoinMoin.support.lupy.index.indexwriter import IndexWriter
17
17
from MoinMoin.support.lupy.search.indexsearcher import IndexSearcher
18
18
 
 
19
from MoinMoin.support.lupy.index.term import Term
 
20
from MoinMoin.support.lupy.search.term import TermQuery
 
21
from MoinMoin.support.lupy.search.boolean import BooleanQuery
 
22
 
19
23
##############################################################################
20
24
### Tokenizer
21
25
##############################################################################
22
26
 
23
 
word_re = re.compile(r"\w+", re.U)
24
 
wikiword_re = re.compile(r"^([%(u)s][%(l)s]+)+$" % {'u': config.chars_upper,
25
 
                                                'l': config.chars_lower}, re.U)
26
 
singleword_re = re.compile(r"[%(u)s][%(l)s]+" % {'u': config.chars_upper,
27
 
                                             'l': config.chars_lower}, re.U)
 
27
singleword = r"[%(u)s][%(l)s]+" % {
 
28
                 'u': config.chars_upper,
 
29
                 'l': config.chars_lower,
 
30
             }
 
31
 
 
32
singleword_re = re.compile(singleword, re.U)
 
33
wikiword_re = re.compile(r"^(%s){2,}$" % singleword, re.U)
28
34
 
29
35
token_re = re.compile(
30
 
    r"(?P<company>\w+[&@]\w+)|" + #company names like AT&T and Excite@Home.
 
36
    r"(?P<company>\w+[&@]\w+)|" + # company names like AT&T and Excite@Home.
31
37
    r"(?P<email>\w+([.-]\w+)*@\w+([.-]\w+)*)|" +    # email addresses
32
38
    r"(?P<hostname>\w+(\.\w+)+)|" +                 # hostnames
33
39
    r"(?P<num>(\w+[-/.,])*\w*\d\w*([-/.,]\w+)*)|" + # version numbers
47
53
        tokenstream = re.finditer(token_re, value)
48
54
        for m in tokenstream:
49
55
            if m.group("acronym"):
50
 
                yield m.group("acronym").replace('.','').lower()
 
56
                yield m.group("acronym").replace('.', '').lower()
51
57
            elif m.group("company"):
52
58
                yield m.group("company").lower()
53
59
            elif m.group("email"):
61
67
                for word in dot_re.split(m.group("num").lower()):
62
68
                    yield word
63
69
            elif m.group("word"):
64
 
                if wikiword_re.match(m.group("word")):
65
 
                    for sm in re.finditer(singleword_re, m.group()):
 
70
                word = m.group("word")
 
71
                yield  word.lower()
 
72
                # if it is a CamelCaseWord, we additionally yield Camel, Case and Word
 
73
                if wikiword_re.match(word):
 
74
                    for sm in re.finditer(singleword_re, word):
66
75
                        yield sm.group().lower()
67
 
                else:
68
 
                    yield  m.group("word").lower()
69
76
 
70
77
 
71
78
#############################################################################
82
89
        return os.path.exists(self.file)
83
90
 
84
91
    def append(self, pagename):
85
 
        """ Append a page to queue 
86
 
        
87
 
        TODO: tune timeout
88
 
        """
 
92
        """ Append a page to queue """
89
93
        if not self.writeLock.acquire(60.0):
90
94
            request.log("can't add %r to lupy update queue: can't lock queue" %
91
95
                        pagename)
100
104
            self.writeLock.release()
101
105
 
102
106
    def pages(self):
103
 
        """ Return list of pages in the queue 
104
 
        
105
 
        TODO: tune timeout
106
 
        """
 
107
        """ Return list of pages in the queue """
107
108
        if self.readLock.acquire(1.0):
108
109
            try:
109
110
                return self._decode(self._read())
116
117
        
117
118
        When the queue is empty, the queue file is removed, so exists()
118
119
        can tell if there is something waiting in the queue.
119
 
        
120
 
        TODO: tune the timeout
121
120
        """
122
121
        if self.writeLock.acquire(30.0):
123
122
            try:
148
147
        unique = []
149
148
        seen = {}
150
149
        for name in pages:
151
 
            if name in seen:
152
 
                continue
153
 
            unique.append(name)
154
 
            seen[name] = 1
 
150
            if not name in seen:
 
151
                unique.append(name)
 
152
                seen[name] = 1
155
153
        return unique
156
154
 
157
155
    def _read(self):
174
172
    def _write(self, pages):
175
173
        """ Write pages to queue file
176
174
        
177
 
        Require queue write locking.
 
175
        Requires queue write locking.
178
176
        """
179
177
        # XXX use tmpfile/move for atomic replace on real operating systems
180
178
        data = '\n'.join(pages) + '\n'
187
185
    def _removeFile(self):
188
186
        """ Remove queue file 
189
187
        
190
 
        Require write locking.
 
188
        Requires queue write locking.
191
189
        """
192
190
        try:
193
191
            os.remove(self.file)
195
193
            if err.errno != errno.ENOENT:
196
194
                raise
197
195
 
 
196
 
198
197
class Index:
199
198
    class LockedException(Exception):
200
199
        pass
202
201
    def __init__(self, request):
203
202
        self.request = request
204
203
        cache_dir = request.cfg.cache_dir
205
 
        self.dir = os.path.join(cache_dir, 'lupy_index')
 
204
        self.main_dir = os.path.join(cache_dir, 'lupy')
 
205
        self.dir = os.path.join(self.main_dir, 'index')
206
206
        filesys.makeDirs(self.dir)
207
 
        self.sig_file = os.path.join(self.dir, '__complete__')
 
207
        self.sig_file = os.path.join(self.main_dir, 'complete')
208
208
        self.segments_file = os.path.join(self.dir, 'segments')
209
 
        lock_dir = os.path.join(cache_dir, 'lupy_index_lock')
 
209
        lock_dir = os.path.join(self.main_dir, 'index-lock')
210
210
        self.lock = lock.WriteLock(lock_dir,
211
211
                                   timeout=3600.0, readlocktimeout=60.0)
212
212
        self.read_lock = lock.ReadLock(lock_dir, timeout=3600.0)
213
 
        self.queue = UpdateQueue(os.path.join(self.dir, "__update_queue__"),
214
 
                                 os.path.join(cache_dir, 'lupy_queue_lock'))
 
213
        self.queue = UpdateQueue(os.path.join(self.main_dir, "update-queue"),
 
214
                                 os.path.join(self.main_dir, 'update-queue-lock'))
215
215
        
216
216
        # Disabled until we have a sane way to build the index with a
217
217
        # queue in small steps.
225
225
    def mtime(self):
226
226
        return os.path.getmtime(self.segments_file)
227
227
 
 
228
    def _search(self, query):
 
229
        """ read lock must be acquired """
 
230
        while True:
 
231
            try:
 
232
                searcher, timestamp = self.request.cfg.lupy_searchers.pop()
 
233
                if timestamp != self.mtime():
 
234
                    searcher.close()
 
235
                else:
 
236
                    break
 
237
            except IndexError:
 
238
                searcher = IndexSearcher(self.dir)
 
239
                timestamp = self.mtime()
 
240
                break
 
241
            
 
242
        hits = list(searcher.search(query))
 
243
        self.request.cfg.lupy_searchers.append((searcher, timestamp))
 
244
        return hits
 
245
    
228
246
    def search(self, query):
229
247
        if not self.read_lock.acquire(1.0):
230
248
            raise self.LockedException
231
249
        try:
232
 
            while True:
233
 
                try:
234
 
                    searcher, timestamp = self.request.cfg.lupy_searchers.pop()
235
 
                    if timestamp!=self.mtime():
236
 
                        searcher.close()
237
 
                    else:
238
 
                        break
239
 
                except IndexError:
240
 
                    searcher = IndexSearcher(self.dir)
241
 
                    timestamp = self.mtime()
242
 
                    break
243
 
                
244
 
            hits = list(searcher.search(query))
245
 
            self.request.cfg.lupy_searchers.append((searcher, timestamp))
 
250
            hits = self._search(query)
246
251
        finally:
247
252
            self.read_lock.release()
248
253
        return hits
249
254
 
250
255
    def update_page(self, page):
 
256
        self.queue.append(page.page_name)
 
257
        self._do_queued_updates_InNewThread()
 
258
 
 
259
    def _do_queued_updates_InNewThread(self):
 
260
        """ do queued index updates in a new thread
 
261
        
 
262
        Should be called from a user request. From a script, use indexPages.
 
263
        """
251
264
        if not self.lock.acquire(1.0):
252
 
            self.queue.append(page.page_name)
 
265
            self.request.log("can't index: can't acquire lock")
253
266
            return
254
 
        self.request.clock.start('update_page')
255
267
        try:
256
 
            self._do_queued_updates()
257
 
            self._update_page(page)
258
 
        finally:
 
268
            from threading import Thread
 
269
            indexThread = Thread(target=self._do_queued_updates,
 
270
                args=(self._indexingRequest(self.request), self.lock))
 
271
            indexThread.setDaemon(True)
 
272
            
 
273
            # Join the index thread after current request finish, prevent
 
274
            # Apache CGI from killing the process.
 
275
            def joinDecorator(finish):
 
276
                def func():
 
277
                    finish()
 
278
                    indexThread.join()
 
279
                return func
 
280
                
 
281
            self.request.finish = joinDecorator(self.request.finish)        
 
282
            indexThread.start()
 
283
        except:
259
284
            self.lock.release()
260
 
        self.request.clock.stop('update_page')
 
285
            raise
261
286
 
262
 
    def indexPages(self):
263
 
        """ Index all pages
 
287
    def indexPages(self, files=None, update=True):
 
288
        """ Index all pages (and files, if given)
264
289
        
265
290
        Can be called only from a script. To index pages during a user
266
 
        request, use indexPagesInNewThread. 
267
 
        
268
 
        TODO: tune the acquire timeout
 
291
        request, use indexPagesInNewThread.
 
292
        @arg files: iterator or list of files to index additionally
 
293
        @arg update: True = update an existing index, False = reindex everything
269
294
        """
270
295
        if not self.lock.acquire(1.0):
271
296
            self.request.log("can't index: can't acquire lock")
272
297
            return
273
298
        try:
274
 
            self._index_pages(self._indexingRequest(self.request))
 
299
            request = self._indexingRequest(self.request)
 
300
            self._index_pages(request, None, files, update)
275
301
        finally:
276
302
            self.lock.release()
277
303
    
278
 
    def indexPagesInNewThread(self):
 
304
    def indexPagesInNewThread(self, files=None, update=True):
279
305
        """ Index all pages in a new thread
280
306
        
281
 
        Should be called from a user request. From a script, use
282
 
        indexPages.
283
 
 
284
 
        TODO: tune the acquire timeout
 
307
        Should be called from a user request. From a script, use indexPages.
285
308
        """
286
309
        if not self.lock.acquire(1.0):
287
310
            self.request.log("can't index: can't acquire lock")
293
316
                return
294
317
            from threading import Thread
295
318
            indexThread = Thread(target=self._index_pages,
296
 
                args=(self._indexingRequest(self.request), self.lock))
 
319
                args=(self._indexingRequest(self.request), self.lock, files, update))
297
320
            indexThread.setDaemon(True)
298
321
            
299
322
            # Join the index thread after current request finish, prevent
332
355
    # -------------------------------------------------------------------
333
356
    # Private
334
357
 
335
 
    def _do_queued_updates(self, amount=5):
 
358
    def _do_queued_updates(self, request, lock=None, amount=5):
336
359
        """ Assumes that the write lock is acquired """
337
 
        pages = self.queue.pages()[:amount]
338
 
        for name in pages:
339
 
            self._update_page(Page(self.request, name))
340
 
        self.queue.remove(pages)
 
360
        try:
 
361
            pages = self.queue.pages()[:amount]
 
362
            for name in pages:
 
363
                p = Page(request, name)
 
364
                self._update_page(p)
 
365
                self.queue.remove([name])
 
366
        finally:
 
367
            if lock:
 
368
                lock.release()
341
369
 
342
370
    def _update_page(self, page):
343
371
        """ Assumes that the write lock is acquired """
346
374
        reader.close()
347
375
        if page.exists():
348
376
            writer = IndexWriter(self.dir, False, tokenizer)
349
 
            self._index_page(writer, page)
 
377
            self._index_page(writer, page, False) # we don't need to check whether it is updated
350
378
            writer.close()
351
 
        
352
 
    def _index_page(self, writer, page):
353
 
        """ Assumes that the write lock is acquired """
354
 
        d = document.Document()
355
 
        d.add(document.Keyword('pagename', page.page_name))
356
 
        d.add(document.Text('title', page.page_name, store=False))        
357
 
        d.add(document.Text('text', page.get_raw_body(), store=False))
358
 
        
359
 
        links = page.getPageLinks(page.request)
360
 
        t = document.Text('links', '', store=False)
361
 
        t.stringVal = links
362
 
        d.add(t)
363
 
        d.add(document.Text('link_text', ' '.join(links), store=False))
364
 
 
365
 
        writer.addDocument(d)
366
 
 
367
 
    def _index_pages(self, request, lock=None):
368
 
        """ Index all pages
369
 
        
370
 
        This should be called from indexPages or indexPagesInNewThread
371
 
        only!
372
 
        
373
 
        This may take few minutes up to few hours, depending on the
374
 
        size of the wiki.
 
379
   
 
380
    def contentfilter(self, filename):
 
381
        """ Get a filter for content of filename and return unicode content. """
 
382
        import mimetypes
 
383
        from MoinMoin import wikiutil
 
384
        request = self.request
 
385
        mimetype, encoding = mimetypes.guess_type(filename)
 
386
        if mimetype is None:
 
387
            mimetype = 'application/octet-stream'
 
388
        def mt2mn(mt): # mimetype to modulename
 
389
            return mt.replace("/", "_").replace("-","_").replace(".", "_")
 
390
        try:
 
391
            _filter = mt2mn(mimetype)
 
392
            execute = wikiutil.importPlugin(request.cfg, 'filter', _filter)
 
393
        except wikiutil.PluginMissingError:
 
394
            try:
 
395
                _filter = mt2mn(mimetype.split("/", 1)[0])
 
396
                execute = wikiutil.importPlugin(request.cfg, 'filter', _filter)
 
397
            except wikiutil.PluginMissingError:
 
398
                try:
 
399
                    _filter = mt2mn('application/octet-stream')
 
400
                    execute = wikiutil.importPlugin(request.cfg, 'filter', _filter)
 
401
                except wikiutil.PluginMissingError:
 
402
                    raise ImportError("Cannot load filter %s" % binaryfilter)
 
403
        try:
 
404
            data = execute(self, filename)
 
405
            request.log("Filter %s returned %d characters for file %s" % (_filter, len(data), filename))
 
406
        except (OSError, IOError), err:
 
407
            data = ''
 
408
            request.log("Filter %s threw error '%s' for file %s" % (_filter, str(err), filename))
 
409
        return data
 
410
   
 
411
    def test(self, request):
 
412
        query = BooleanQuery()
 
413
        query.add(TermQuery(Term("text", 'suchmich')), True, False)
 
414
        docs = self._search(query)
 
415
        for d in docs:
 
416
            request.log("%r %r %r" % (d, d.get('attachment'), d.get('pagename')))
 
417
 
 
418
    def _index_file(self, request, writer, filename, update):
 
419
        """ index a file as it were a page named pagename
 
420
            Assumes that the write lock is acquired
 
421
        """
 
422
        fs_rootpage = 'FS' # XXX FS hardcoded
 
423
        try:
 
424
            mtime = os.path.getmtime(filename)
 
425
            mtime = wikiutil.timestamp2version(mtime)
 
426
            if update:
 
427
                query = BooleanQuery()
 
428
                query.add(TermQuery(Term("pagename", fs_rootpage)), True, False)
 
429
                query.add(TermQuery(Term("attachment", filename)), True, False)
 
430
                docs = self._search(query)
 
431
                updated = len(docs) == 0 or mtime > int(docs[0].get('mtime'))
 
432
            else:
 
433
                updated = True
 
434
            request.log("%s %r" % (filename, updated))
 
435
            if updated:
 
436
                file_content = self.contentfilter(filename)
 
437
                d = document.Document()
 
438
                d.add(document.Keyword('pagename', fs_rootpage))
 
439
                d.add(document.Keyword('mtime', str(mtime)))
 
440
                d.add(document.Keyword('attachment', filename)) # XXX we should treat files like real pages, not attachments
 
441
                pagename = " ".join(os.path.join(fs_rootpage, filename).split("/"))
 
442
                d.add(document.Text('title', pagename, store=False))        
 
443
                d.add(document.Text('text', file_content, store=False))
 
444
                writer.addDocument(d)
 
445
        except (OSError, IOError), err:
 
446
            pass
 
447
 
 
448
    def _index_page(self, writer, page, update):
 
449
        """ Index a page - assumes that the write lock is acquired
 
450
            @arg writer: the index writer object
 
451
            @arg page: a page object
 
452
            @arg update: False = index in any case, True = index only when changed
 
453
        """
 
454
        pagename = page.page_name
 
455
        request = page.request
 
456
        mtime = page.mtime_usecs()
 
457
        if update:
 
458
            query = BooleanQuery()
 
459
            query.add(TermQuery(Term("pagename", pagename)), True, False)
 
460
            query.add(TermQuery(Term("attachment", "")), True, False)
 
461
            docs = self._search(query)
 
462
            updated = len(docs) == 0 or mtime > int(docs[0].get('mtime'))
 
463
        else:
 
464
            updated = True
 
465
        request.log("%s %r" % (pagename, updated))
 
466
        if updated:
 
467
            d = document.Document()
 
468
            d.add(document.Keyword('pagename', pagename))
 
469
            d.add(document.Keyword('mtime', str(mtime)))
 
470
            d.add(document.Keyword('attachment', '')) # this is a real page, not an attachment
 
471
            d.add(document.Text('title', pagename, store=False))        
 
472
            d.add(document.Text('text', page.get_raw_body(), store=False))
 
473
            
 
474
            links = page.getPageLinks(request)
 
475
            t = document.Text('links', '', store=False)
 
476
            t.stringVal = links
 
477
            d.add(t)
 
478
            d.add(document.Text('link_text', ' '.join(links), store=False))
 
479
 
 
480
            writer.addDocument(d)
 
481
        
 
482
        from MoinMoin.action import AttachFile
 
483
 
 
484
        attachments = AttachFile._get_files(request, pagename)
 
485
        for att in attachments:
 
486
            filename = AttachFile.getFilename(request, pagename, att)
 
487
            mtime = wikiutil.timestamp2version(os.path.getmtime(filename))
 
488
            if update:
 
489
                query = BooleanQuery()
 
490
                query.add(TermQuery(Term("pagename", pagename)), True, False)
 
491
                query.add(TermQuery(Term("attachment", att)), True, False)
 
492
                docs = self._search(query)
 
493
                updated = len(docs) == 0 or mtime > int(docs[0].get('mtime'))
 
494
            else:
 
495
                updated = True
 
496
            request.log("%s %s %r" % (pagename, att, updated))
 
497
            if updated:
 
498
                att_content = self.contentfilter(filename)
 
499
                d = document.Document()
 
500
                d.add(document.Keyword('pagename', pagename))
 
501
                d.add(document.Keyword('mtime', str(mtime)))
 
502
                d.add(document.Keyword('attachment', att)) # this is an attachment, store its filename
 
503
                d.add(document.Text('title', att, store=False)) # the filename is the "title" of an attachment
 
504
                d.add(document.Text('text', att_content, store=False))
 
505
                writer.addDocument(d)
 
506
 
 
507
 
 
508
    def _index_pages(self, request, lock=None, files=None, update=True):
 
509
        """ Index all pages (and all given files)
 
510
        
 
511
        This should be called from indexPages or indexPagesInNewThread only!
 
512
        
 
513
        This may take few minutes up to few hours, depending on the size of
 
514
        the wiki.
375
515
 
376
516
        When called in a new thread, lock is acquired before the call,
377
517
        and this method must release it when it finishes or fails.
379
519
        try:
380
520
            self._unsign()
381
521
            start = time.time()
382
 
            writer = IndexWriter(self.dir, True, tokenizer)
383
 
            writer.mergeFactor = 200
 
522
            writer = IndexWriter(self.dir, not update, tokenizer)
 
523
            writer.mergeFactor = 50
384
524
            pages = request.rootpage.getPageList(user='', exists=1)
385
525
            request.log("indexing all (%d) pages..." % len(pages))
386
526
            for pagename in pages:
387
 
                # Some code assumes request.page
388
 
                request.page = Page(request, pagename)
389
 
                self._index_page(writer, request.page)
 
527
                p = Page(request, pagename)
 
528
                # code does NOT seem to assume request.page being set any more
 
529
                #request.page = p
 
530
                self._index_page(writer, p, update)
 
531
            if files:
 
532
                request.log("indexing all files...")
 
533
                for fname in files:
 
534
                    fname = fname.strip()
 
535
                    self._index_file(request, writer, fname, update)
390
536
            writer.close()
391
537
            request.log("indexing completed successfully in %0.2f seconds." % 
392
538
                        (time.time() - start))
398
544
 
399
545
    def _optimize(self, request):
400
546
        """ Optimize the index """
 
547
        self._unsign()
401
548
        start = time.time()
402
549
        request.log("optimizing index...")
403
550
        writer = IndexWriter(self.dir, False, tokenizer)
405
552
        writer.close()
406
553
        request.log("optimizing completed successfully in %0.2f seconds." % 
407
554
                    (time.time() - start))
 
555
        self._sign()
408
556
 
409
557
    def _indexingRequest(self, request):
410
558
        """ Return a new request that can be used for index building.