Distutils installation script.
[redakcja.git] / lib / vstorage.py
index d67e2c1..2274a04 100644 (file)
@@ -1,9 +1,17 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.  
+#
 import os
 import tempfile
 import datetime
 import mimetypes
 import urllib
 import os
 import tempfile
 import datetime
 import mimetypes
 import urllib
+import functools
+
+import logging
+logger = logging.getLogger('fnp.hazlenut.vstorage')
 
 # Note: we have to set these before importing Mercurial
 os.environ['HGENCODING'] = 'utf-8'
 
 # Note: we have to set these before importing Mercurial
 os.environ['HGENCODING'] = 'utf-8'
@@ -15,7 +23,7 @@ import mercurial.revlog
 import mercurial.util
 
 
 import mercurial.util
 
 
-def urlquote(url, safe='/'):
+def urlquote(url, safe = '/'):
     """Quotes URL 
     
     >>> urlquote(u'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144')
     """Quotes URL 
     
     >>> urlquote(u'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144')
@@ -42,27 +50,36 @@ def find_repo_path(path):
     return path
 
 
     return path
 
 
-def locked_repo(func):
+def with_working_copy_locked(func):
     """A decorator for locking the repository when calling a method."""
 
     """A decorator for locking the repository when calling a method."""
 
-    def new_func(self, *args, **kwargs):
+    @functools.wraps(func)
+    def wrapped(self, *args, **kwargs):
         """Wrap the original function in locks."""
         """Wrap the original function in locks."""
-
         wlock = self.repo.wlock()
         wlock = self.repo.wlock()
-        lock = self.repo.lock()
         try:
         try:
-            func(self, *args, **kwargs)
-        finally:
-            lock.release()
+            return func(self, *args, **kwargs)
+        finally:            
             wlock.release()
             wlock.release()
+    return wrapped
 
 
-    return new_func
+def with_storage_locked(func):
+    """A decorator for locking the repository when calling a method."""
 
 
+    @functools.wraps(func)
+    def wrapped(self, *args, **kwargs):
+        """Wrap the original function in locks."""
+        lock = self.repo.lock()
+        try:
+            return func(self, *args, **kwargs)
+        finally:            
+            lock.release()
+    return wrapped
 
 def guess_mime(file_name):
     """
     Guess file's mime type based on extension.
 
 def guess_mime(file_name):
     """
     Guess file's mime type based on extension.
-    Default ot text/x-wiki for files without an extension.
+    Default of text/x-wiki for files without an extension.
 
     >>> guess_mime('something.txt')
     'text/plain'
 
     >>> guess_mime('something.txt')
     'text/plain'
@@ -78,7 +95,7 @@ def guess_mime(file_name):
     'archive/gzip'
     """
 
     'archive/gzip'
     """
 
-    mime, encoding = mimetypes.guess_type(file_name, strict=False)
+    mime, encoding = mimetypes.guess_type(file_name, strict = False)
     if encoding:
         mime = 'archive/%s' % encoding
     if mime is None:
     if encoding:
         mime = 'archive/%s' % encoding
     if mime is None:
@@ -96,7 +113,7 @@ class VersionedStorage(object):
     change history, using Mercurial repository as the storage method.
     """
 
     change history, using Mercurial repository as the storage method.
     """
 
-    def __init__(self, path, charset=None):
+    def __init__(self, path, charset = None):
         """
         Takes the path to the directory where the pages are to be kept.
         If the directory doen't exist, it will be created. If it's inside
         """
         Takes the path to the directory where the pages are to be kept.
         If the directory doen't exist, it will be created. If it's inside
@@ -109,46 +126,43 @@ class VersionedStorage(object):
         if not os.path.exists(self.path):
             os.makedirs(self.path)
         self.repo_path = find_repo_path(self.path)
         if not os.path.exists(self.path):
             os.makedirs(self.path)
         self.repo_path = find_repo_path(self.path)
-        try:
-            self.ui = mercurial.ui.ui(report_untrusted=False,
-                                      interactive=False, quiet=True)
-        except TypeError:
-            # Mercurial 1.3 changed the way we setup the ui object.
-            self.ui = mercurial.ui.ui()
-            self.ui.quiet = True
-            self.ui._report_untrusted = False
-            self.ui.setconfig('ui', 'interactive', False)
+        
+        self.ui = mercurial.ui.ui()
+        self.ui.quiet = True
+        self.ui._report_untrusted = False
+        self.ui.setconfig('ui', 'interactive', False)
+        
         if self.repo_path is None:
             self.repo_path = self.path
             create = True
         else:
             create = False
         if self.repo_path is None:
             self.repo_path = self.path
             create = True
         else:
             create = False
+            
         self.repo_prefix = self.path[len(self.repo_path):].strip('/')
         self.repo = mercurial.hg.repository(self.ui, self.repo_path,
         self.repo_prefix = self.path[len(self.repo_path):].strip('/')
         self.repo = mercurial.hg.repository(self.ui, self.repo_path,
-                                            create=create)
+                                            create = create)
 
     def reopen(self):
         """Close and reopen the repo, to make sure we are up to date."""
 
     def reopen(self):
         """Close and reopen the repo, to make sure we are up to date."""
-
         self.repo = mercurial.hg.repository(self.ui, self.repo_path)
 
     def _file_path(self, title):
         self.repo = mercurial.hg.repository(self.ui, self.repo_path)
 
     def _file_path(self, title):
-        return os.path.join(self.path, urlquote(title, safe=''))
+        return os.path.join(self.path, urlquote(title, safe = ''))
 
     def _title_to_file(self, title):
 
     def _title_to_file(self, title):
-        return os.path.join(self.repo_prefix, urlquote(title, safe=''))
+        return os.path.join(self.repo_prefix, urlquote(title, safe = ''))
 
     def _file_to_title(self, filename):
         assert filename.startswith(self.repo_prefix)
         name = filename[len(self.repo_prefix):].strip('/')
         return urlunquote(name)
 
 
     def _file_to_title(self, filename):
         assert filename.startswith(self.repo_prefix)
         name = filename[len(self.repo_prefix):].strip('/')
         return urlunquote(name)
 
-    def __contains__(self, title):
-        return urlquote(title) in self.repo.dirstate
+    def __contains__(self, title):                        
+        return urlquote(title) in self.repo['tip']
 
     def __iter__(self):
 
     def __iter__(self):
-        return self.all_pages()        
-        
+        return self.all_pages()
+
     def merge_changes(self, changectx, repo_file, text, user, parent):
         """Commits and merges conflicting changes in the repository."""
         tip_node = changectx.node()
     def merge_changes(self, changectx, repo_file, text, user, parent):
         """Commits and merges conflicting changes in the repository."""
         tip_node = changectx.node()
@@ -157,20 +171,20 @@ class VersionedStorage(object):
 
         self.repo.dirstate.setparents(parent_node)
         node = self._commit([repo_file], text, user)
 
         self.repo.dirstate.setparents(parent_node)
         node = self._commit([repo_file], text, user)
-        
+
         partial = lambda filename: repo_file == filename
         partial = lambda filename: repo_file == filename
-        
+
         # If p1 is equal to p2, there is no work to do. Even the dirstate is correct.
         p1, p2 = self.repo[None].parents()[0], self.repo[tip_node]
         if p1 == p2:
             return text
         # If p1 is equal to p2, there is no work to do. Even the dirstate is correct.
         p1, p2 = self.repo[None].parents()[0], self.repo[tip_node]
         if p1 == p2:
             return text
-        
+
         try:
             mercurial.merge.update(self.repo, tip_node, True, False, partial)
             msg = 'merge of edit conflict'
         except mercurial.util.Abort:
             msg = 'failed merge of edit conflict'
         try:
             mercurial.merge.update(self.repo, tip_node, True, False, partial)
             msg = 'merge of edit conflict'
         except mercurial.util.Abort:
             msg = 'failed merge of edit conflict'
-        
+
         self.repo.dirstate.setparents(tip_node, node)
         # Mercurial 1.1 and later need updating the merge state
         try:
         self.repo.dirstate.setparents(tip_node, node)
         # Mercurial 1.1 and later need updating the merge state
         try:
@@ -179,50 +193,42 @@ class VersionedStorage(object):
             pass
         return msg
 
             pass
         return msg
 
-    @locked_repo
-    def save_file(self, title, file_name, author=u'', comment=u'', parent=None):
+    @with_working_copy_locked
+    @with_storage_locked
+    def save_file(self, title, file_name, author = u'', comment = u'', parent = None):
         """Save an existing file as specified page."""
         """Save an existing file as specified page."""
-
-        user = author.encode('utf-8') or u'anon'.encode('utf-8')
+        user = author.encode('utf-8') or u'anonymous'.encode('utf-8')
         text = comment.encode('utf-8') or u'comment'.encode('utf-8')
         text = comment.encode('utf-8') or u'comment'.encode('utf-8')
+        
         repo_file = self._title_to_file(title)
         file_path = self._file_path(title)
         mercurial.util.rename(file_name, file_path)
         changectx = self._changectx()
         repo_file = self._title_to_file(title)
         file_path = self._file_path(title)
         mercurial.util.rename(file_name, file_path)
         changectx = self._changectx()
+        
         try:
             filectx_tip = changectx[repo_file]
             current_page_rev = filectx_tip.filerev()
         except mercurial.revlog.LookupError:
             self.repo.add([repo_file])
             current_page_rev = -1
         try:
             filectx_tip = changectx[repo_file]
             current_page_rev = filectx_tip.filerev()
         except mercurial.revlog.LookupError:
             self.repo.add([repo_file])
             current_page_rev = -1
+        
         if parent is not None and current_page_rev != parent:
             msg = self.merge_changes(changectx, repo_file, text, user, parent)
             user = '<wiki>'
             text = msg.encode('utf-8')
         if parent is not None and current_page_rev != parent:
             msg = self.merge_changes(changectx, repo_file, text, user, parent)
             user = '<wiki>'
             text = msg.encode('utf-8')
+            
         self._commit([repo_file], text, user)
         self._commit([repo_file], text, user)
-
-
-    def _commit(self, files, text, user):
-        try:
-            return self.repo.commit(files=files, text=text, user=user,
-                                    force=True, empty_ok=True)
-        except TypeError:
-            # Mercurial 1.3 doesn't accept empty_ok or files parameter
-            match = mercurial.match.exact(self.repo_path, '', list(files))
-            return self.repo.commit(match=match, text=text, user=user,
-                                    force=True)
-
-
-    def save_data(self, title, data, author=u'', comment=u'', parent=None):
+        
+        
+    def save_data(self, title, data, **kwargs):
         """Save data as specified page."""
         """Save data as specified page."""
-
         try:
         try:
-            temp_path = tempfile.mkdtemp(dir=self.path)
+            temp_path = tempfile.mkdtemp(dir = self.path)
             file_path = os.path.join(temp_path, 'saved')
             f = open(file_path, "wb")
             f.write(data)
             f.close()
             file_path = os.path.join(temp_path, 'saved')
             f = open(file_path, "wb")
             f.write(data)
             f.close()
-            self.save_file(title, file_path, author, comment, parent)
+            self.save_file(title = title, file_name = file_path, **kwargs)
         finally:
             try:
                 os.unlink(file_path)
         finally:
             try:
                 os.unlink(file_path)
@@ -233,15 +239,17 @@ class VersionedStorage(object):
             except OSError:
                 pass
 
             except OSError:
                 pass
 
-    def save_text(self, title, text, author=u'', comment=u'', parent=None):
-        """Save text as specified page, encoded to charset."""
+    def save_text(self, text, **kwargs):
+        """Save text as specified page, encoded to charset.""" 
+        self.save_data(data = text.encode(self.charset), **kwargs)
 
 
-        data = text.encode(self.charset)
-        self.save_data(title, data, author, comment, parent)
 
 
+    def _commit(self, files, text, user):
+        match = mercurial.match.exact(self.repo_path, '', list(files))
+        return self.repo.commit(match = match, text = text, user = user, force = True)
     def page_text(self, title):
         """Read unicode text of a page."""
     def page_text(self, title):
         """Read unicode text of a page."""
-
         data = self.open_page(title).read()
         text = unicode(data, self.charset, 'replace')
         return text
         data = self.open_page(title).read()
         text = unicode(data, self.charset, 'replace')
         return text
@@ -250,8 +258,9 @@ class VersionedStorage(object):
         for data in page:
             yield unicode(data, self.charset, 'replace')
 
         for data in page:
             yield unicode(data, self.charset, 'replace')
 
-    @locked_repo
-    def delete_page(self, title, author=u'', comment=u''):
+    @with_working_copy_locked
+    @with_storage_locked
+    def delete_page(self, title, author = u'', comment = u''):
         user = author.encode('utf-8') or 'anon'
         text = comment.encode('utf-8') or 'deleted'
         repo_file = self._title_to_file(title)
         user = author.encode('utf-8') or 'anon'
         text = comment.encode('utf-8') or 'deleted'
         repo_file = self._title_to_file(title)
@@ -263,33 +272,38 @@ class VersionedStorage(object):
         self.repo.remove([repo_file])
         self._commit([repo_file], text, user)
 
         self.repo.remove([repo_file])
         self._commit([repo_file], text, user)
 
+    @with_working_copy_locked
     def open_page(self, title):
         if title not in self:
     def open_page(self, title):
         if title not in self:
-            print 'whatever', list(self.all_pages())
             raise DocumentNotFound()
         
             raise DocumentNotFound()
         
+        path = self._title_to_file(title)
+        logger.debug("Opening page %s", path)      
         try:
         try:
-            return open(self._file_path(title), "rb")
+            return self.repo.wfile(path, 'rb')            
         except IOError:
         except IOError:
+            logger.exception("Failed to open page %s", title)
             raise DocumentNotFound()
 
             raise DocumentNotFound()
 
+    @with_working_copy_locked
     def page_file_meta(self, title):
         """Get page's inode number, size and last modification time."""
     def page_file_meta(self, title):
         """Get page's inode number, size and last modification time."""
-
         try:
             (st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size,
              st_atime, st_mtime, st_ctime) = os.stat(self._file_path(title))
         except OSError:
             return 0, 0, 0
         return st_ino, st_size, st_mtime
         try:
             (st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size,
              st_atime, st_mtime, st_ctime) = os.stat(self._file_path(title))
         except OSError:
             return 0, 0, 0
         return st_ino, st_size, st_mtime
-
+    
+    @with_working_copy_locked
     def page_meta(self, title):
         """Get page's revision, date, last editor and his edit comment."""
     def page_meta(self, title):
         """Get page's revision, date, last editor and his edit comment."""
+        if not title in self:
+            raise DocumentNotFound()
 
         filectx_tip = self._find_filectx(title)
         if filectx_tip is None:
             raise DocumentNotFound()
 
         filectx_tip = self._find_filectx(title)
         if filectx_tip is None:
             raise DocumentNotFound()
-            #return -1, None, u'', u''
         rev = filectx_tip.filerev()
         filectx = filectx_tip.filectx(rev)
         date = datetime.datetime.fromtimestamp(filectx.date()[0])
         rev = filectx_tip.filerev()
         filectx = filectx_tip.filectx(rev)
         date = datetime.datetime.fromtimestamp(filectx.date()[0])
@@ -299,23 +313,17 @@ class VersionedStorage(object):
         return rev, date, author, comment
 
     def repo_revision(self):
         return rev, date, author, comment
 
     def repo_revision(self):
-        return self._changectx().rev()
+        return self.repo['tip'].rev()
+    
+    def _changectx(self):
+        return self.repo['tip']
 
     def page_mime(self, title):
         """
         Guess page's mime type based on corresponding file name.
         Default ot text/x-wiki for files without an extension.
         """
 
     def page_mime(self, title):
         """
         Guess page's mime type based on corresponding file name.
         Default ot text/x-wiki for files without an extension.
         """
-        return guess_type(self._file_path(title))
-
-    def _changectx(self):
-        """Get the changectx of the tip."""
-        try:
-            # This is for Mercurial 1.0
-            return self.repo.changectx()
-        except TypeError:
-            # Mercurial 1.3 (and possibly earlier) needs an argument
-            return self.repo.changectx('tip')
+        return guess_mime(self._file_path(title))
 
     def _find_filectx(self, title):
         """Find the last revision in which the file existed."""
 
     def _find_filectx(self, title):
         """Find the last revision in which the file existed."""
@@ -340,13 +348,13 @@ class VersionedStorage(object):
             return
         maxrev = filectx_tip.filerev()
         minrev = 0
             return
         maxrev = filectx_tip.filerev()
         minrev = 0
-        for rev in range(maxrev, minrev-1, -1):
+        for rev in range(maxrev, minrev - 1, -1):
             filectx = filectx_tip.filectx(rev)
             date = datetime.datetime.fromtimestamp(filectx.date()[0])
             author = unicode(filectx.user(), "utf-8",
                              'replace').split('<')[0].strip()
             comment = unicode(filectx.description(), "utf-8", 'replace')
             filectx = filectx_tip.filectx(rev)
             date = datetime.datetime.fromtimestamp(filectx.date()[0])
             author = unicode(filectx.user(), "utf-8",
                              'replace').split('<')[0].strip()
             comment = unicode(filectx.description(), "utf-8", 'replace')
-            yield rev, date, author, comment
+            yield {"version": rev, "date": date, "author": author, "description": comment}
 
     def page_revision(self, title, rev):
         """Get unicode contents of specified revision of the page."""
 
     def page_revision(self, title, rev):
         """Get unicode contents of specified revision of the page."""
@@ -371,7 +379,7 @@ class VersionedStorage(object):
         changectx = self._changectx()
         maxrev = changectx.rev()
         minrev = 0
         changectx = self._changectx()
         maxrev = changectx.rev()
         minrev = 0
-        for wiki_rev in range(maxrev, minrev-1, -1):
+        for wiki_rev in range(maxrev, minrev - 1, -1):
             change = self.repo.changectx(wiki_rev)
             date = datetime.datetime.fromtimestamp(change.date()[0])
             author = unicode(change.user(), "utf-8",
             change = self.repo.changectx(wiki_rev)
             date = datetime.datetime.fromtimestamp(change.date()[0])
             author = unicode(change.user(), "utf-8",
@@ -387,11 +395,9 @@ class VersionedStorage(object):
                     yield title, rev, date, author, comment
 
     def all_pages(self):
                     yield title, rev, date, author, comment
 
     def all_pages(self):
+        tip = self.repo['tip']
         """Iterate over the titles of all pages in the wiki."""
         """Iterate over the titles of all pages in the wiki."""
-        status = self.repo.status(self.repo[None], None, None, True, True, True)
-        clean_files = status[6]
-        for filename in clean_files:
-            yield urlunquote(filename)
+        return [ urlunquote(filename) for filename in tip ]        
 
     def changed_since(self, rev):
         """Return all pages that changed since specified repository revision."""
 
     def changed_since(self, rev):
         """Return all pages that changed since specified repository revision."""
@@ -405,6 +411,6 @@ class VersionedStorage(object):
         current = self.repo.lookup('tip')
         status = self.repo.status(current, last)
         modified, added, removed, deleted, unknown, ignored, clean = status
         current = self.repo.lookup('tip')
         status = self.repo.status(current, last)
         modified, added, removed, deleted, unknown, ignored, clean = status
-        for filename in modified+added+removed+deleted:
+        for filename in modified + added + removed + deleted:
             if filename.startswith(self.repo_prefix):
                 yield self._file_to_title(filename)
             if filename.startswith(self.repo_prefix):
                 yield self._file_to_title(filename)