CedarBackup2.extend.subversion

Package CedarBackup2 :: Package extend :: Module subversion |
[hide private]
[frames] | no frames]

# Source Code for Module CedarBackup2.extend.subversion

       1  # -*- coding: iso-8859-1 -*-
       2  # vim: set ft=python ts=3 sw=3 expandtab:
       3  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
       4  #
       5  #              C E D A R
       6  #          S O L U T I O N S       "Software done right."
       7  #           S O F T W A R E
       8  #
       9  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
      10  #
      11  # Copyright (c) 2005,2007,2010 Kenneth J. Pronovici.
      12  # All rights reserved.
      13  #
      14  # This program is free software; you can redistribute it and/or
      15  # modify it under the terms of the GNU General Public License,
      16  # Version 2, as published by the Free Software Foundation.
      17  #
      18  # This program is distributed in the hope that it will be useful,
      19  # but WITHOUT ANY WARRANTY; without even the implied warranty of
      20  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
      21  #
      22  # Copies of the GNU General Public License are available from
      23  # the Free Software Foundation website, http://www.gnu.org/.
      24  #
      25  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
      26  #
      27  # Author   : Kenneth J. Pronovici <pronovic@ieee.org>
      28  # Language : Python 2 (>= 2.7)
      29  # Project  : Official Cedar Backup Extensions
      30  # Purpose  : Provides an extension to back up Subversion repositories.
      31  #
      32  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
      33
      34  ########################################################################
      35  # Module documentation
      36  ########################################################################
      37
      38  """
      39  Provides an extension to back up Subversion repositories.
      40
      41  This is a Cedar Backup extension used to back up Subversion repositories via
      42  the Cedar Backup command line.  Each Subversion repository can be backed using
      43  the same collect modes allowed for filesystems in the standard Cedar Backup
      44  collect action: weekly, daily, incremental.
      45
      46  This extension requires a new configuration section <subversion> and is
      47  intended to be run either immediately before or immediately after the standard
      48  collect action.  Aside from its own configuration, it requires the options and
      49  collect configuration sections in the standard Cedar Backup configuration file.
      50
      51  There are two different kinds of Subversion repositories at this writing: BDB
      52  (Berkeley Database) and FSFS (a "filesystem within a filesystem").  Although
      53  the repository type can be specified in configuration, that information is just
      54  kept around for reference.  It doesn't affect the backup.  Both kinds of
      55  repositories are backed up in the same way, using C{svnadmin dump} in an
      56  incremental mode.
      57
      58  It turns out that FSFS repositories can also be backed up just like any
      59  other filesystem directory.  If you would rather do that, then use the normal
      60  collect action.  This is probably simpler, although it carries its own
      61  advantages and disadvantages (plus you will have to be careful to exclude
      62  the working directories Subversion uses when building an update to commit).
      63  Check the Subversion documentation for more information.
      64
      65  @author: Kenneth J. Pronovici <pronovic@ieee.org>
      66  """
      67
      68  ########################################################################
      69  # Imported modules
      70  ########################################################################
      71
      72  # System modules
      73  import os
      74  import logging
      75  import pickle
      76  from bz2 import BZ2File
      77  from gzip import GzipFile
      78
      79  # Cedar Backup modules
      80  from CedarBackup2.xmlutil import createInputDom, addContainerNode, addStringNode
      81  from CedarBackup2.xmlutil import isElement, readChildren, readFirstChild, readString, readStringList
      82  from CedarBackup2.config import VALID_COLLECT_MODES, VALID_COMPRESS_MODES
      83  from CedarBackup2.filesystem import FilesystemList
      84  from CedarBackup2.util import UnorderedList, RegexList
      85  from CedarBackup2.util import isStartOfWeek, buildNormalizedPath
      86  from CedarBackup2.util import resolveCommand, executeCommand
      87  from CedarBackup2.util import ObjectTypeList, encodePath, changeOwnership
      88
      89
      90  ########################################################################
      91  # Module-wide constants and variables
      92  ########################################################################
      93
      94  logger = logging.getLogger("CedarBackup2.log.extend.subversion")
      95
      96  SVNLOOK_COMMAND      = [ "svnlook", ]
      97  SVNADMIN_COMMAND     = [ "svnadmin", ]
      98
      99  REVISION_PATH_EXTENSION = "svnlast"
     100
     101
     102  ########################################################################
     103  # RepositoryDir class definition
     104  ########################################################################
     105
     106 -class RepositoryDir(object):
     107
     108     """
     109     Class representing Subversion repository directory.
     110
     111     A repository directory is a directory that contains one or more Subversion
     112     repositories.
     113
     114     The following restrictions exist on data in this class:
     115
     116        - The directory path must be absolute.
     117        - The collect mode must be one of the values in L{VALID_COLLECT_MODES}.
     118        - The compress mode must be one of the values in L{VALID_COMPRESS_MODES}.
     119
     120     The repository type value is kept around just for reference.  It doesn't
     121     affect the behavior of the backup.
     122
     123     Relative exclusions are allowed here.  However, there is no configured
     124     ignore file, because repository dir backups are not recursive.
     125
     126     @sort: __init__, __repr__, __str__, __cmp__, directoryPath, collectMode, compressMode
     127     """
     128
     129 -   def __init__(self, repositoryType=None, directoryPath=None, collectMode=None, compressMode=None,
     130                  relativeExcludePaths=None, excludePatterns=None):
     131        """
     132        Constructor for the C{RepositoryDir} class.
     133
     134        @param repositoryType: Type of repository, for reference
     135        @param directoryPath: Absolute path of the Subversion parent directory
     136        @param collectMode: Overridden collect mode for this directory.
     137        @param compressMode: Overridden compression mode for this directory.
     138        @param relativeExcludePaths: List of relative paths to exclude.
     139        @param excludePatterns: List of regular expression patterns to exclude
     140        """
     141        self._repositoryType = None
     142        self._directoryPath = None
     143        self._collectMode = None
     144        self._compressMode = None
     145        self._relativeExcludePaths = None
     146        self._excludePatterns = None
     147        self.repositoryType = repositoryType
     148        self.directoryPath = directoryPath
     149        self.collectMode = collectMode
     150        self.compressMode = compressMode
     151        self.relativeExcludePaths = relativeExcludePaths
     152        self.excludePatterns = excludePatterns
     153
     154 -   def __repr__(self):
     155        """
     156        Official string representation for class instance.
     157        """
     158        return "RepositoryDir(%s, %s, %s, %s, %s, %s)" % (self.repositoryType, self.directoryPath, self.collectMode,
     159                                                          self.compressMode, self.relativeExcludePaths, self.excludePatterns)
     160
     161 -   def __str__(self):
     162        """
     163        Informal string representation for class instance.
     164        """
     165        return self.__repr__()
     166
     167 -   def __cmp__(self, other):
     168        """
     169        Definition of equals operator for this class.
     170        @param other: Other object to compare to.
     171        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
     172        """
     173        if other is None:
     174           return 1
     175        if self.repositoryType != other.repositoryType:
     176           if self.repositoryType < other.repositoryType:
     177              return -1
     178           else:
     179              return 1
     180        if self.directoryPath != other.directoryPath:
     181           if self.directoryPath < other.directoryPath:
     182              return -1
     183           else:
     184              return 1
     185        if self.collectMode != other.collectMode:
     186           if self.collectMode < other.collectMode:
     187              return -1
     188           else:
     189              return 1
     190        if self.compressMode != other.compressMode:
     191           if self.compressMode < other.compressMode:
     192              return -1
     193           else:
     194              return 1
     195        if self.relativeExcludePaths != other.relativeExcludePaths:
     196           if self.relativeExcludePaths < other.relativeExcludePaths:
     197              return -1
     198           else:
     199              return 1
     200        if self.excludePatterns != other.excludePatterns:
     201           if self.excludePatterns < other.excludePatterns:
     202              return -1
     203           else:
     204              return 1
     205        return 0
     206
     207 -   def _setRepositoryType(self, value):
     208        """
     209        Property target used to set the repository type.
     210        There is no validation; this value is kept around just for reference.
     211        """
     212        self._repositoryType = value
     213
     214 -   def _getRepositoryType(self):
     215        """
     216        Property target used to get the repository type.
     217        """
     218        return self._repositoryType
     219
     220 -   def _setDirectoryPath(self, value):
     221        """
     222        Property target used to set the directory path.
     223        The value must be an absolute path if it is not C{None}.
     224        It does not have to exist on disk at the time of assignment.
     225        @raise ValueError: If the value is not an absolute path.
     226        @raise ValueError: If the value cannot be encoded properly.
     227        """
     228        if value is not None:
     229           if not os.path.isabs(value):
     230              raise ValueError("Repository path must be an absolute path.")
     231        self._directoryPath = encodePath(value)
     232
     233 -   def _getDirectoryPath(self):
     234        """
     235        Property target used to get the repository path.
     236        """
     237        return self._directoryPath
     238
     239 -   def _setCollectMode(self, value):
     240        """
     241        Property target used to set the collect mode.
     242        If not C{None}, the mode must be one of the values in L{VALID_COLLECT_MODES}.
     243        @raise ValueError: If the value is not valid.
     244        """
     245        if value is not None:
     246           if value not in VALID_COLLECT_MODES:
     247              raise ValueError("Collect mode must be one of %s." % VALID_COLLECT_MODES)
     248        self._collectMode = value
     249
     250 -   def _getCollectMode(self):
     251        """
     252        Property target used to get the collect mode.
     253        """
     254        return self._collectMode
     255
     256 -   def _setCompressMode(self, value):
     257        """
     258        Property target used to set the compress mode.
     259        If not C{None}, the mode must be one of the values in L{VALID_COMPRESS_MODES}.
     260        @raise ValueError: If the value is not valid.
     261        """
     262        if value is not None:
     263           if value not in VALID_COMPRESS_MODES:
     264              raise ValueError("Compress mode must be one of %s." % VALID_COMPRESS_MODES)
     265        self._compressMode = value
     266
     267 -   def _getCompressMode(self):
     268        """
     269        Property target used to get the compress mode.
     270        """
     271        return self._compressMode
     272
     273 -   def _setRelativeExcludePaths(self, value):
     274        """
     275        Property target used to set the relative exclude paths list.
     276        Elements do not have to exist on disk at the time of assignment.
     277        """
     278        if value is None:
     279           self._relativeExcludePaths = None
     280        else:
     281           try:
     282              saved = self._relativeExcludePaths
     283              self._relativeExcludePaths = UnorderedList()
     284              self._relativeExcludePaths.extend(value)
     285           except Exception, e:
     286              self._relativeExcludePaths = saved
     287              raise e
     288
     289 -   def _getRelativeExcludePaths(self):
     290        """
     291        Property target used to get the relative exclude paths list.
     292        """
     293        return self._relativeExcludePaths
     294
     295 -   def _setExcludePatterns(self, value):
     296        """
     297        Property target used to set the exclude patterns list.
     298        """
     299        if value is None:
     300           self._excludePatterns = None
     301        else:
     302           try:
     303              saved = self._excludePatterns
     304              self._excludePatterns = RegexList()
     305              self._excludePatterns.extend(value)
     306           except Exception, e:
     307              self._excludePatterns = saved
     308              raise e
     309
     310 -   def _getExcludePatterns(self):
     311        """
     312        Property target used to get the exclude patterns list.
     313        """
     314        return self._excludePatterns
     315
     316     repositoryType = property(_getRepositoryType, _setRepositoryType, None, doc="Type of this repository, for reference.")
     317     directoryPath = property(_getDirectoryPath, _setDirectoryPath, None, doc="Absolute path of the Subversion parent directory.")
     318     collectMode = property(_getCollectMode, _setCollectMode, None, doc="Overridden collect mode for this repository.")
     319     compressMode = property(_getCompressMode, _setCompressMode, None, doc="Overridden compress mode for this repository.")
     320     relativeExcludePaths = property(_getRelativeExcludePaths, _setRelativeExcludePaths, None, "List of relative paths to exclude.")
     321     excludePatterns = property(_getExcludePatterns, _setExcludePatterns, None, "List of regular expression patterns to exclude.")
     322
     323
     324  ########################################################################
     325  # Repository class definition
     326  ########################################################################
     327
     328 -class Repository(object):
     329
     330     """
     331     Class representing generic Subversion repository configuration..
     332
     333     The following restrictions exist on data in this class:
     334
     335        - The respository path must be absolute.
     336        - The collect mode must be one of the values in L{VALID_COLLECT_MODES}.
     337        - The compress mode must be one of the values in L{VALID_COMPRESS_MODES}.
     338
     339     The repository type value is kept around just for reference.  It doesn't
     340     affect the behavior of the backup.
     341
     342     @sort: __init__, __repr__, __str__, __cmp__, repositoryPath, collectMode, compressMode
     343     """
     344
     345 -   def __init__(self, repositoryType=None, repositoryPath=None, collectMode=None, compressMode=None):
     346        """
     347        Constructor for the C{Repository} class.
     348
     349        @param repositoryType: Type of repository, for reference
     350        @param repositoryPath: Absolute path to a Subversion repository on disk.
     351        @param collectMode: Overridden collect mode for this directory.
     352        @param compressMode: Overridden compression mode for this directory.
     353        """
     354        self._repositoryType = None
     355        self._repositoryPath = None
     356        self._collectMode = None
     357        self._compressMode = None
     358        self.repositoryType = repositoryType
     359        self.repositoryPath = repositoryPath
     360        self.collectMode = collectMode
     361        self.compressMode = compressMode
     362
     363 -   def __repr__(self):
     364        """
     365        Official string representation for class instance.
     366        """
     367        return "Repository(%s, %s, %s, %s)" % (self.repositoryType, self.repositoryPath, self.collectMode, self.compressMode)
     368
     369 -   def __str__(self):
     370        """
     371        Informal string representation for class instance.
     372        """
     373        return self.__repr__()
     374
     375 -   def __cmp__(self, other):
     376        """
     377        Definition of equals operator for this class.
     378        @param other: Other object to compare to.
     379        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
     380        """
     381        if other is None:
     382           return 1
     383        if self.repositoryType != other.repositoryType:
     384           if self.repositoryType < other.repositoryType:
     385              return -1
     386           else:
     387              return 1
     388        if self.repositoryPath != other.repositoryPath:
     389           if self.repositoryPath < other.repositoryPath:
     390              return -1
     391           else:
     392              return 1
     393        if self.collectMode != other.collectMode:
     394           if self.collectMode < other.collectMode:
     395              return -1
     396           else:
     397              return 1
     398        if self.compressMode != other.compressMode:
     399           if self.compressMode < other.compressMode:
     400              return -1
     401           else:
     402              return 1
     403        return 0
     404
     405 -   def _setRepositoryType(self, value):
     406        """
     407        Property target used to set the repository type.
     408        There is no validation; this value is kept around just for reference.
     409        """
     410        self._repositoryType = value
     411
     412 -   def _getRepositoryType(self):
     413        """
     414        Property target used to get the repository type.
     415        """
     416        return self._repositoryType
     417
     418 -   def _setRepositoryPath(self, value):
     419        """
     420        Property target used to set the repository path.
     421        The value must be an absolute path if it is not C{None}.
     422        It does not have to exist on disk at the time of assignment.
     423        @raise ValueError: If the value is not an absolute path.
     424        @raise ValueError: If the value cannot be encoded properly.
     425        """
     426        if value is not None:
     427           if not os.path.isabs(value):
     428              raise ValueError("Repository path must be an absolute path.")
     429        self._repositoryPath = encodePath(value)
     430
     431 -   def _getRepositoryPath(self):
     432        """
     433        Property target used to get the repository path.
     434        """
     435        return self._repositoryPath
     436
     437 -   def _setCollectMode(self, value):
     438        """
     439        Property target used to set the collect mode.
     440        If not C{None}, the mode must be one of the values in L{VALID_COLLECT_MODES}.
     441        @raise ValueError: If the value is not valid.
     442        """
     443        if value is not None:
     444           if value not in VALID_COLLECT_MODES:
     445              raise ValueError("Collect mode must be one of %s." % VALID_COLLECT_MODES)
     446        self._collectMode = value
     447
     448 -   def _getCollectMode(self):
     449        """
     450        Property target used to get the collect mode.
     451        """
     452        return self._collectMode
     453
     454 -   def _setCompressMode(self, value):
     455        """
     456        Property target used to set the compress mode.
     457        If not C{None}, the mode must be one of the values in L{VALID_COMPRESS_MODES}.
     458        @raise ValueError: If the value is not valid.
     459        """
     460        if value is not None:
     461           if value not in VALID_COMPRESS_MODES:
     462              raise ValueError("Compress mode must be one of %s." % VALID_COMPRESS_MODES)
     463        self._compressMode = value
     464
     465 -   def _getCompressMode(self):
     466        """
     467        Property target used to get the compress mode.
     468        """
     469        return self._compressMode
     470
     471     repositoryType = property(_getRepositoryType, _setRepositoryType, None, doc="Type of this repository, for reference.")
     472     repositoryPath = property(_getRepositoryPath, _setRepositoryPath, None, doc="Path to the repository to collect.")
     473     collectMode = property(_getCollectMode, _setCollectMode, None, doc="Overridden collect mode for this repository.")
     474     compressMode = property(_getCompressMode, _setCompressMode, None, doc="Overridden compress mode for this repository.")
     475
     476
     477  ########################################################################
     478  # SubversionConfig class definition
     479  ########################################################################
     480
     481 -class SubversionConfig(object):
     482
     483     """
     484     Class representing Subversion configuration.
     485
     486     Subversion configuration is used for backing up Subversion repositories.
     487
     488     The following restrictions exist on data in this class:
     489
     490        - The collect mode must be one of the values in L{VALID_COLLECT_MODES}.
     491        - The compress mode must be one of the values in L{VALID_COMPRESS_MODES}.
     492        - The repositories list must be a list of C{Repository} objects.
     493        - The repositoryDirs list must be a list of C{RepositoryDir} objects.
     494
     495     For the two lists, validation is accomplished through the
     496     L{util.ObjectTypeList} list implementation that overrides common list
     497     methods and transparently ensures that each element has the correct type.
     498
     499     @note: Lists within this class are "unordered" for equality comparisons.
     500
     501     @sort: __init__, __repr__, __str__, __cmp__, collectMode, compressMode, repositories
     502     """
     503
     504 -   def __init__(self, collectMode=None, compressMode=None, repositories=None, repositoryDirs=None):
     505        """
     506        Constructor for the C{SubversionConfig} class.
     507
     508        @param collectMode: Default collect mode.
     509        @param compressMode: Default compress mode.
     510        @param repositories: List of Subversion repositories to back up.
     511        @param repositoryDirs: List of Subversion parent directories to back up.
     512
     513        @raise ValueError: If one of the values is invalid.
     514        """
     515        self._collectMode = None
     516        self._compressMode = None
     517        self._repositories = None
     518        self._repositoryDirs = None
     519        self.collectMode = collectMode
     520        self.compressMode = compressMode
     521        self.repositories = repositories
     522        self.repositoryDirs = repositoryDirs
     523
     524 -   def __repr__(self):
     525        """
     526        Official string representation for class instance.
     527        """
     528        return "SubversionConfig(%s, %s, %s, %s)" % (self.collectMode, self.compressMode, self.repositories, self.repositoryDirs)
     529
     530 -   def __str__(self):
     531        """
     532        Informal string representation for class instance.
     533        """
     534        return self.__repr__()
     535
     536 -   def __cmp__(self, other):
     537        """
     538        Definition of equals operator for this class.
     539        Lists within this class are "unordered" for equality comparisons.
     540        @param other: Other object to compare to.
     541        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
     542        """
     543        if other is None:
     544           return 1
     545        if self.collectMode != other.collectMode:
     546           if self.collectMode < other.collectMode:
     547              return -1
     548           else:
     549              return 1
     550        if self.compressMode != other.compressMode:
     551           if self.compressMode < other.compressMode:
     552              return -1
     553           else:
     554              return 1
     555        if self.repositories != other.repositories:
     556           if self.repositories < other.repositories:
     557              return -1
     558           else:
     559              return 1
     560        if self.repositoryDirs != other.repositoryDirs:
     561           if self.repositoryDirs < other.repositoryDirs:
     562              return -1
     563           else:
     564              return 1
     565        return 0
     566
     567 -   def _setCollectMode(self, value):
     568        """
     569        Property target used to set the collect mode.
     570        If not C{None}, the mode must be one of the values in L{VALID_COLLECT_MODES}.
     571        @raise ValueError: If the value is not valid.
     572        """
     573        if value is not None:
     574           if value not in VALID_COLLECT_MODES:
     575              raise ValueError("Collect mode must be one of %s." % VALID_COLLECT_MODES)
     576        self._collectMode = value
     577
     578 -   def _getCollectMode(self):
     579        """
     580        Property target used to get the collect mode.
     581        """
     582        return self._collectMode
     583
     584 -   def _setCompressMode(self, value):
     585        """
     586        Property target used to set the compress mode.
     587        If not C{None}, the mode must be one of the values in L{VALID_COMPRESS_MODES}.
     588        @raise ValueError: If the value is not valid.
     589        """
     590        if value is not None:
     591           if value not in VALID_COMPRESS_MODES:
     592              raise ValueError("Compress mode must be one of %s." % VALID_COMPRESS_MODES)
     593        self._compressMode = value
     594
     595 -   def _getCompressMode(self):
     596        """
     597        Property target used to get the compress mode.
     598        """
     599        return self._compressMode
     600
     601 -   def _setRepositories(self, value):
     602        """
     603        Property target used to set the repositories list.
     604        Either the value must be C{None} or each element must be a C{Repository}.
     605        @raise ValueError: If the value is not a C{Repository}
     606        """
     607        if value is None:
     608           self._repositories = None
     609        else:
     610           try:
     611              saved = self._repositories
     612              self._repositories = ObjectTypeList(Repository, "Repository")
     613              self._repositories.extend(value)
     614           except Exception, e:
     615              self._repositories = saved
     616              raise e
     617
     618 -   def _getRepositories(self):
     619        """
     620        Property target used to get the repositories list.
     621        """
     622        return self._repositories
     623
     624 -   def _setRepositoryDirs(self, value):
     625        """
     626        Property target used to set the repositoryDirs list.
     627        Either the value must be C{None} or each element must be a C{Repository}.
     628        @raise ValueError: If the value is not a C{Repository}
     629        """
     630        if value is None:
     631           self._repositoryDirs = None
     632        else:
     633           try:
     634              saved = self._repositoryDirs
     635              self._repositoryDirs = ObjectTypeList(RepositoryDir, "RepositoryDir")
     636              self._repositoryDirs.extend(value)
     637           except Exception, e:
     638              self._repositoryDirs = saved
     639              raise e
     640
     641 -   def _getRepositoryDirs(self):
     642        """
     643        Property target used to get the repositoryDirs list.
     644        """
     645        return self._repositoryDirs
     646
     647     collectMode = property(_getCollectMode, _setCollectMode, None, doc="Default collect mode.")
     648     compressMode = property(_getCompressMode, _setCompressMode, None, doc="Default compress mode.")
     649     repositories = property(_getRepositories, _setRepositories, None, doc="List of Subversion repositories to back up.")
     650     repositoryDirs = property(_getRepositoryDirs, _setRepositoryDirs, None, doc="List of Subversion parent directories to back up.")
     651
     652
     653  ########################################################################
     654  # LocalConfig class definition
     655  ########################################################################
     656
     657 -class LocalConfig(object):
     658
     659     """
     660     Class representing this extension's configuration document.
     661
     662     This is not a general-purpose configuration object like the main Cedar
     663     Backup configuration object.  Instead, it just knows how to parse and emit
     664     Subversion-specific configuration values.  Third parties who need to read
     665     and write configuration related to this extension should access it through
     666     the constructor, C{validate} and C{addConfig} methods.
     667
     668     @note: Lists within this class are "unordered" for equality comparisons.
     669
     670     @sort: __init__, __repr__, __str__, __cmp__, subversion, validate, addConfig
     671     """
     672
     673 -   def __init__(self, xmlData=None, xmlPath=None, validate=True):
     674        """
     675        Initializes a configuration object.
     676
     677        If you initialize the object without passing either C{xmlData} or
     678        C{xmlPath} then configuration will be empty and will be invalid until it
     679        is filled in properly.
     680
     681        No reference to the original XML data or original path is saved off by
     682        this class.  Once the data has been parsed (successfully or not) this
     683        original information is discarded.
     684
     685        Unless the C{validate} argument is C{False}, the L{LocalConfig.validate}
     686        method will be called (with its default arguments) against configuration
     687        after successfully parsing any passed-in XML.  Keep in mind that even if
     688        C{validate} is C{False}, it might not be possible to parse the passed-in
     689        XML document if lower-level validations fail.
     690
     691        @note: It is strongly suggested that the C{validate} option always be set
     692        to C{True} (the default) unless there is a specific need to read in
     693        invalid configuration from disk.
     694
     695        @param xmlData: XML data representing configuration.
     696        @type xmlData: String data.
     697
     698        @param xmlPath: Path to an XML file on disk.
     699        @type xmlPath: Absolute path to a file on disk.
     700
     701        @param validate: Validate the document after parsing it.
     702        @type validate: Boolean true/false.
     703
     704        @raise ValueError: If both C{xmlData} and C{xmlPath} are passed-in.
     705        @raise ValueError: If the XML data in C{xmlData} or C{xmlPath} cannot be parsed.
     706        @raise ValueError: If the parsed configuration document is not valid.
     707        """
     708        self._subversion = None
     709        self.subversion = None
     710        if xmlData is not None and xmlPath is not None:
     711           raise ValueError("Use either xmlData or xmlPath, but not both.")
     712        if xmlData is not None:
     713           self._parseXmlData(xmlData)
     714           if validate:
     715              self.validate()
     716        elif xmlPath is not None:
     717           xmlData = open(xmlPath).read()
     718           self._parseXmlData(xmlData)
     719           if validate:
     720              self.validate()
     721
     722 -   def __repr__(self):
     723        """
     724        Official string representation for class instance.
     725        """
     726        return "LocalConfig(%s)" % (self.subversion)
     727
     728 -   def __str__(self):
     729        """
     730        Informal string representation for class instance.
     731        """
     732        return self.__repr__()
     733
     734 -   def __cmp__(self, other):
     735        """
     736        Definition of equals operator for this class.
     737        Lists within this class are "unordered" for equality comparisons.
     738        @param other: Other object to compare to.
     739        @return: -1/0/1 depending on whether self is C{<}, C{=} or C{>} other.
     740        """
     741        if other is None:
     742           return 1
     743        if self.subversion != other.subversion:
     744           if self.subversion < other.subversion:
     745              return -1
     746           else:
     747              return 1
     748        return 0
     749
     750 -   def _setSubversion(self, value):
     751        """
     752        Property target used to set the subversion configuration value.
     753        If not C{None}, the value must be a C{SubversionConfig} object.
     754        @raise ValueError: If the value is not a C{SubversionConfig}
     755        """
     756        if value is None:
     757           self._subversion = None
     758        else:
     759           if not isinstance(value, SubversionConfig):
     760              raise ValueError("Value must be a C{SubversionConfig} object.")
     761           self._subversion = value
     762
     763 -   def _getSubversion(self):
     764        """
     765        Property target used to get the subversion configuration value.
     766        """
     767        return self._subversion
     768
     769     subversion = property(_getSubversion, _setSubversion, None, "Subversion configuration in terms of a C{SubversionConfig} object.")
     770
     771 -   def validate(self):
     772        """
     773        Validates configuration represented by the object.
     774
     775        Subversion configuration must be filled in.  Within that, the collect
     776        mode and compress mode are both optional, but the list of repositories
     777        must contain at least one entry.
     778
     779        Each repository must contain a repository path, and then must be either
     780        able to take collect mode and compress mode configuration from the parent
     781        C{SubversionConfig} object, or must set each value on its own.
     782
     783        @raise ValueError: If one of the validations fails.
     784        """
     785        if self.subversion is None:
     786           raise ValueError("Subversion section is required.")
     787        if ((self.subversion.repositories is None or len(self.subversion.repositories) < 1) and
     788            (self.subversion.repositoryDirs is None or len(self.subversion.repositoryDirs) <1)):
     789           raise ValueError("At least one Subversion repository must be configured.")
     790        if self.subversion.repositories is not None:
     791           for repository in self.subversion.repositories:
     792              if repository.repositoryPath is None:
     793                 raise ValueError("Each repository must set a repository path.")
     794              if self.subversion.collectMode is None and repository.collectMode is None:
     795                 raise ValueError("Collect mode must either be set in parent section or individual repository.")
     796              if self.subversion.compressMode is None and repository.compressMode is None:
     797                 raise ValueError("Compress mode must either be set in parent section or individual repository.")
     798        if self.subversion.repositoryDirs is not None:
     799           for repositoryDir in self.subversion.repositoryDirs:
     800              if repositoryDir.directoryPath is None:
     801                 raise ValueError("Each repository directory must set a directory path.")
     802              if self.subversion.collectMode is None and repositoryDir.collectMode is None:
     803                 raise ValueError("Collect mode must either be set in parent section or repository directory.")
     804              if self.subversion.compressMode is None and repositoryDir.compressMode is None:
     805                 raise ValueError("Compress mode must either be set in parent section or repository directory.")
     806
     807 -   def addConfig(self, xmlDom, parentNode):
     808        """
     809        Adds a <subversion> configuration section as the next child of a parent.
     810
     811        Third parties should use this function to write configuration related to
     812        this extension.
     813
     814        We add the following fields to the document::
     815
     816           collectMode    //cb_config/subversion/collectMode
     817           compressMode   //cb_config/subversion/compressMode
     818
     819        We also add groups of the following items, one list element per
     820        item::
     821
     822           repository     //cb_config/subversion/repository
     823           repository_dir //cb_config/subversion/repository_dir
     824
     825        @param xmlDom: DOM tree as from C{impl.createDocument()}.
     826        @param parentNode: Parent that the section should be appended to.
     827        """
     828        if self.subversion is not None:
     829           sectionNode = addContainerNode(xmlDom, parentNode, "subversion")
     830           addStringNode(xmlDom, sectionNode, "collect_mode", self.subversion.collectMode)
     831           addStringNode(xmlDom, sectionNode, "compress_mode", self.subversion.compressMode)
     832           if self.subversion.repositories is not None:
     833              for repository in self.subversion.repositories:
     834                 LocalConfig._addRepository(xmlDom, sectionNode, repository)
     835           if self.subversion.repositoryDirs is not None:
     836              for repositoryDir in self.subversion.repositoryDirs:
     837                 LocalConfig._addRepositoryDir(xmlDom, sectionNode, repositoryDir)
     838
     839 -   def _parseXmlData(self, xmlData):
     840        """
     841        Internal method to parse an XML string into the object.
     842
     843        This method parses the XML document into a DOM tree (C{xmlDom}) and then
     844        calls a static method to parse the subversion configuration section.
     845
     846        @param xmlData: XML data to be parsed
     847        @type xmlData: String data
     848
     849        @raise ValueError: If the XML cannot be successfully parsed.
     850        """
     851        (xmlDom, parentNode) = createInputDom(xmlData)
     852        self._subversion = LocalConfig._parseSubversion(parentNode)
     853
     854     @staticmethod
     855 -   def _parseSubversion(parent):
     856        """
     857        Parses a subversion configuration section.
     858
     859        We read the following individual fields::
     860
     861           collectMode    //cb_config/subversion/collect_mode
     862           compressMode   //cb_config/subversion/compress_mode
     863
     864        We also read groups of the following item, one list element per
     865        item::
     866
     867           repositories    //cb_config/subversion/repository
     868           repository_dirs //cb_config/subversion/repository_dir
     869
     870        The repositories are parsed by L{_parseRepositories}, and the repository
     871        dirs are parsed by L{_parseRepositoryDirs}.
     872
     873        @param parent: Parent node to search beneath.
     874
     875        @return: C{SubversionConfig} object or C{None} if the section does not exist.
     876        @raise ValueError: If some filled-in value is invalid.
     877        """
     878        subversion = None
     879        section = readFirstChild(parent, "subversion")
     880        if section is not None:
     881           subversion = SubversionConfig()
     882           subversion.collectMode = readString(section, "collect_mode")
     883           subversion.compressMode = readString(section, "compress_mode")
     884           subversion.repositories = LocalConfig._parseRepositories(section)
     885           subversion.repositoryDirs = LocalConfig._parseRepositoryDirs(section)
     886        return subversion
     887
     888     @staticmethod
     889 -   def _parseRepositories(parent):
     890        """
     891        Reads a list of C{Repository} objects from immediately beneath the parent.
     892
     893        We read the following individual fields::
     894
     895           repositoryType          type
     896           repositoryPath          abs_path
     897           collectMode             collect_mode
     898           compressMode            compess_mode
     899
     900        The type field is optional, and its value is kept around only for
     901        reference.
     902
     903        @param parent: Parent node to search beneath.
     904
     905        @return: List of C{Repository} objects or C{None} if none are found.
     906        @raise ValueError: If some filled-in value is invalid.
     907        """
     908        lst = []
     909        for entry in readChildren(parent, "repository"):
     910           if isElement(entry):
     911              repository = Repository()
     912              repository.repositoryType = readString(entry, "type")
     913              repository.repositoryPath = readString(entry, "abs_path")
     914              repository.collectMode = readString(entry, "collect_mode")
     915              repository.compressMode = readString(entry, "compress_mode")
     916              lst.append(repository)
     917        if lst == []:
     918           lst = None
     919        return lst
     920
     921     @staticmethod
     922 -   def _addRepository(xmlDom, parentNode, repository):
     923        """
     924        Adds a repository container as the next child of a parent.
     925
     926        We add the following fields to the document::
     927
     928           repositoryType          repository/type
     929           repositoryPath          repository/abs_path
     930           collectMode             repository/collect_mode
     931           compressMode            repository/compress_mode
     932
     933        The <repository> node itself is created as the next child of the parent
     934        node.  This method only adds one repository node.  The parent must loop
     935        for each repository in the C{SubversionConfig} object.
     936
     937        If C{repository} is C{None}, this method call will be a no-op.
     938
     939        @param xmlDom: DOM tree as from C{impl.createDocument()}.
     940        @param parentNode: Parent that the section should be appended to.
     941        @param repository: Repository to be added to the document.
     942        """
     943        if repository is not None:
     944           sectionNode = addContainerNode(xmlDom, parentNode, "repository")
     945           addStringNode(xmlDom, sectionNode, "type", repository.repositoryType)
     946           addStringNode(xmlDom, sectionNode, "abs_path", repository.repositoryPath)
     947           addStringNode(xmlDom, sectionNode, "collect_mode", repository.collectMode)
     948           addStringNode(xmlDom, sectionNode, "compress_mode", repository.compressMode)
     949
     950     @staticmethod
     951 -   def _parseRepositoryDirs(parent):
     952        """
     953        Reads a list of C{RepositoryDir} objects from immediately beneath the parent.
     954
     955        We read the following individual fields::
     956
     957           repositoryType          type
     958           directoryPath           abs_path
     959           collectMode             collect_mode
     960           compressMode            compess_mode
     961
     962        We also read groups of the following items, one list element per
     963        item::
     964
     965           relativeExcludePaths    exclude/rel_path
     966           excludePatterns         exclude/pattern
     967
     968        The exclusions are parsed by L{_parseExclusions}.
     969
     970        The type field is optional, and its value is kept around only for
     971        reference.
     972
     973        @param parent: Parent node to search beneath.
     974
     975        @return: List of C{RepositoryDir} objects or C{None} if none are found.
     976        @raise ValueError: If some filled-in value is invalid.
     977        """
     978        lst = []
     979        for entry in readChildren(parent, "repository_dir"):
     980           if isElement(entry):
     981              repositoryDir = RepositoryDir()
     982              repositoryDir.repositoryType = readString(entry, "type")
     983              repositoryDir.directoryPath = readString(entry, "abs_path")
     984              repositoryDir.collectMode = readString(entry, "collect_mode")
     985              repositoryDir.compressMode = readString(entry, "compress_mode")
     986              (repositoryDir.relativeExcludePaths, repositoryDir.excludePatterns) = LocalConfig._parseExclusions(entry)
     987              lst.append(repositoryDir)
     988        if lst == []:
     989           lst = None
     990        return lst
     991
     992     @staticmethod
     993 -   def _parseExclusions(parentNode):
     994        """
     995        Reads exclusions data from immediately beneath the parent.
     996
     997        We read groups of the following items, one list element per item::
     998
     999           relative    exclude/rel_path
    1000           patterns    exclude/pattern
    1001
    1002        If there are none of some pattern (i.e. no relative path items) then
    1003        C{None} will be returned for that item in the tuple.
    1004
    1005        @param parentNode: Parent node to search beneath.
    1006
    1007        @return: Tuple of (relative, patterns) exclusions.
    1008        """
    1009        section = readFirstChild(parentNode, "exclude")
    1010        if section is None:
    1011           return (None, None)
    1012        else:
    1013           relative = readStringList(section, "rel_path")
    1014           patterns = readStringList(section, "pattern")
    1015           return (relative, patterns)
    1016
    1017     @staticmethod
    1018 -   def _addRepositoryDir(xmlDom, parentNode, repositoryDir):
    1019        """
    1020        Adds a repository dir container as the next child of a parent.
    1021
    1022        We add the following fields to the document::
    1023
    1024           repositoryType          repository_dir/type
    1025           directoryPath           repository_dir/abs_path
    1026           collectMode             repository_dir/collect_mode
    1027           compressMode            repository_dir/compress_mode
    1028
    1029        We also add groups of the following items, one list element per item::
    1030
    1031           relativeExcludePaths    dir/exclude/rel_path
    1032           excludePatterns         dir/exclude/pattern
    1033
    1034        The <repository_dir> node itself is created as the next child of the
    1035        parent node.  This method only adds one repository node.  The parent must
    1036        loop for each repository dir in the C{SubversionConfig} object.
    1037
    1038        If C{repositoryDir} is C{None}, this method call will be a no-op.
    1039
    1040        @param xmlDom: DOM tree as from C{impl.createDocument()}.
    1041        @param parentNode: Parent that the section should be appended to.
    1042        @param repositoryDir: Repository dir to be added to the document.
    1043        """
    1044        if repositoryDir is not None:
    1045           sectionNode = addContainerNode(xmlDom, parentNode, "repository_dir")
    1046           addStringNode(xmlDom, sectionNode, "type", repositoryDir.repositoryType)
    1047           addStringNode(xmlDom, sectionNode, "abs_path", repositoryDir.directoryPath)
    1048           addStringNode(xmlDom, sectionNode, "collect_mode", repositoryDir.collectMode)
    1049           addStringNode(xmlDom, sectionNode, "compress_mode", repositoryDir.compressMode)
    1050           if ((repositoryDir.relativeExcludePaths is not None and repositoryDir.relativeExcludePaths != []) or
    1051               (repositoryDir.excludePatterns is not None and repositoryDir.excludePatterns != [])):
    1052              excludeNode = addContainerNode(xmlDom, sectionNode, "exclude")
    1053              if repositoryDir.relativeExcludePaths is not None:
    1054                 for relativePath in repositoryDir.relativeExcludePaths:
    1055                    addStringNode(xmlDom, excludeNode, "rel_path", relativePath)
    1056              if repositoryDir.excludePatterns is not None:
    1057                 for pattern in repositoryDir.excludePatterns:
    1058                    addStringNode(xmlDom, excludeNode, "pattern", pattern)
    1059
    1060
    1061  ########################################################################
    1062  # Public functions
    1063  ########################################################################
    1064
    1065  ###########################
    1066  # executeAction() function
    1067  ###########################
    1068
    1069 -def executeAction(configPath, options, config):
    1070     """
    1071     Executes the Subversion backup action.
    1072
    1073     @param configPath: Path to configuration file on disk.
    1074     @type configPath: String representing a path on disk.
    1075
    1076     @param options: Program command-line options.
    1077     @type options: Options object.
    1078
    1079     @param config: Program configuration.
    1080     @type config: Config object.
    1081
    1082     @raise ValueError: Under many generic error conditions
    1083     @raise IOError: If a backup could not be written for some reason.
    1084     """
    1085     logger.debug("Executing Subversion extended action.")
    1086     if config.options is None or config.collect is None:
    1087        raise ValueError("Cedar Backup configuration is not properly filled in.")
    1088     local = LocalConfig(xmlPath=configPath)
    1089     todayIsStart = isStartOfWeek(config.options.startingDay)
    1090     fullBackup = options.full or todayIsStart
    1091     logger.debug("Full backup flag is [%s]", fullBackup)
    1092     if local.subversion.repositories is not None:
    1093        for repository in local.subversion.repositories:
    1094           _backupRepository(config, local, todayIsStart, fullBackup, repository)
    1095     if local.subversion.repositoryDirs is not None:
    1096        for repositoryDir in local.subversion.repositoryDirs:
    1097           logger.debug("Working with repository directory [%s].", repositoryDir.directoryPath)
    1098           for repositoryPath in _getRepositoryPaths(repositoryDir):
    1099              repository = Repository(repositoryDir.repositoryType, repositoryPath,
    1100                                      repositoryDir.collectMode, repositoryDir.compressMode)
    1101              _backupRepository(config, local, todayIsStart, fullBackup, repository)
    1102           logger.info("Completed backing up Subversion repository directory [%s].", repositoryDir.directoryPath)
    1103     logger.info("Executed the Subversion extended action successfully.")
    1104
    1105 -def _getCollectMode(local, repository):
    1106     """
    1107     Gets the collect mode that should be used for a repository.
    1108     Use repository's if possible, otherwise take from subversion section.
    1109     @param repository: Repository object.
    1110     @return: Collect mode to use.
    1111     """
    1112     if repository.collectMode is None:
    1113        collectMode = local.subversion.collectMode
    1114     else:
    1115        collectMode = repository.collectMode
    1116     logger.debug("Collect mode is [%s]", collectMode)
    1117     return collectMode
    1118
    1119 -def _getCompressMode(local, repository):
    1120     """
    1121     Gets the compress mode that should be used for a repository.
    1122     Use repository's if possible, otherwise take from subversion section.
    1123     @param local: LocalConfig object.
    1124     @param repository: Repository object.
    1125     @return: Compress mode to use.
    1126     """
    1127     if repository.compressMode is None:
    1128        compressMode = local.subversion.compressMode
    1129     else:
    1130        compressMode = repository.compressMode
    1131     logger.debug("Compress mode is [%s]", compressMode)
    1132     return compressMode
    1133
    1134 -def _getRevisionPath(config, repository):
    1135     """
    1136     Gets the path to the revision file associated with a repository.
    1137     @param config: Config object.
    1138     @param repository: Repository object.
    1139     @return: Absolute path to the revision file associated with the repository.
    1140     """
    1141     normalized = buildNormalizedPath(repository.repositoryPath)
    1142     filename = "%s.%s" % (normalized, REVISION_PATH_EXTENSION)
    1143     revisionPath = os.path.join(config.options.workingDir, filename)
    1144     logger.debug("Revision file path is [%s]", revisionPath)
    1145     return revisionPath
    1146
    1147 -def _getBackupPath(config, repositoryPath, compressMode, startRevision, endRevision):
    1148     """
    1149     Gets the backup file path (including correct extension) associated with a repository.
    1150     @param config: Config object.
    1151     @param repositoryPath: Path to the indicated repository
    1152     @param compressMode: Compress mode to use for this repository.
    1153     @param startRevision: Starting repository revision.
    1154     @param endRevision: Ending repository revision.
    1155     @return: Absolute path to the backup file associated with the repository.
    1156     """
    1157     normalizedPath = buildNormalizedPath(repositoryPath)
    1158     filename = "svndump-%d:%d-%s.txt" % (startRevision, endRevision, normalizedPath)
    1159     if compressMode == 'gzip':
    1160        filename = "%s.gz" % filename
    1161     elif compressMode == 'bzip2':
    1162        filename = "%s.bz2" % filename
    1163     backupPath = os.path.join(config.collect.targetDir, filename)
    1164     logger.debug("Backup file path is [%s]", backupPath)
    1165     return backupPath
    1166
    1167 -def _getRepositoryPaths(repositoryDir):
    1168     """
    1169     Gets a list of child repository paths within a repository directory.
    1170     @param repositoryDir: RepositoryDirectory
    1171     """
    1172     (excludePaths, excludePatterns) = _getExclusions(repositoryDir)
    1173     fsList = FilesystemList()
    1174     fsList.excludeFiles = True
    1175     fsList.excludeLinks = True
    1176     fsList.excludePaths = excludePaths
    1177     fsList.excludePatterns = excludePatterns
    1178     fsList.addDirContents(path=repositoryDir.directoryPath, recursive=False, addSelf=False)
    1179     return fsList
    1180
    1181 -def _getExclusions(repositoryDir):
    1182     """
    1183     Gets exclusions (file and patterns) associated with an repository directory.
    1184
    1185     The returned files value is a list of absolute paths to be excluded from the
    1186     backup for a given directory.  It is derived from the repository directory's
    1187     relative exclude paths.
    1188
    1189     The returned patterns value is a list of patterns to be excluded from the
    1190     backup for a given directory.  It is derived from the repository directory's
    1191     list of patterns.
    1192
    1193     @param repositoryDir: Repository directory object.
    1194
    1195     @return: Tuple (files, patterns) indicating what to exclude.
    1196     """
    1197     paths = []
    1198     if repositoryDir.relativeExcludePaths is not None:
    1199        for relativePath in repositoryDir.relativeExcludePaths:
    1200           paths.append(os.path.join(repositoryDir.directoryPath, relativePath))
    1201     patterns = []
    1202     if repositoryDir.excludePatterns is not None:
    1203        patterns.extend(repositoryDir.excludePatterns)
    1204     logger.debug("Exclude paths: %s", paths)
    1205     logger.debug("Exclude patterns: %s", patterns)
    1206     return(paths, patterns)
    1207
    1208 -def _backupRepository(config, local, todayIsStart, fullBackup, repository):
    1209     """
    1210     Backs up an individual Subversion repository.
    1211
    1212     This internal method wraps the public methods and adds some functionality
    1213     to work better with the extended action itself.
    1214
    1215     @param config: Cedar Backup configuration.
    1216     @param local: Local configuration
    1217     @param todayIsStart: Indicates whether today is start of week
    1218     @param fullBackup: Full backup flag
    1219     @param repository: Repository to operate on
    1220
    1221     @raise ValueError: If some value is missing or invalid.
    1222     @raise IOError: If there is a problem executing the Subversion dump.
    1223     """
    1224     logger.debug("Working with repository [%s]", repository.repositoryPath)
    1225     logger.debug("Repository type is [%s]", repository.repositoryType)
    1226     collectMode = _getCollectMode(local, repository)
    1227     compressMode = _getCompressMode(local, repository)
    1228     revisionPath = _getRevisionPath(config, repository)
    1229     if not (fullBackup or (collectMode in ['daily', 'incr', ]) or (collectMode == 'weekly' and todayIsStart)):
    1230        logger.debug("Repository will not be backed up, per collect mode.")
    1231        return
    1232     logger.debug("Repository meets criteria to be backed up today.")
    1233     if collectMode != "incr" or fullBackup:
    1234        startRevision = 0
    1235        endRevision = getYoungestRevision(repository.repositoryPath)
    1236        logger.debug("Using full backup, revision: (%d, %d).", startRevision, endRevision)
    1237     else:
    1238        if fullBackup:
    1239           startRevision = 0
    1240           endRevision = getYoungestRevision(repository.repositoryPath)
    1241        else:
    1242           startRevision = _loadLastRevision(revisionPath) + 1
    1243           endRevision = getYoungestRevision(repository.repositoryPath)
    1244           if startRevision > endRevision:
    1245              logger.info("No need to back up repository [%s]; no new revisions.", repository.repositoryPath)
    1246              return
    1247        logger.debug("Using incremental backup, revision: (%d, %d).", startRevision, endRevision)
    1248     backupPath = _getBackupPath(config, repository.repositoryPath, compressMode, startRevision, endRevision)
    1249     outputFile = _getOutputFile(backupPath, compressMode)
    1250     try:
    1251        backupRepository(repository.repositoryPath, outputFile, startRevision, endRevision)
    1252     finally:
    1253        outputFile.close()
    1254     if not os.path.exists(backupPath):
    1255        raise IOError("Dump file [%s] does not seem to exist after backup completed." % backupPath)
    1256     changeOwnership(backupPath, config.options.backupUser, config.options.backupGroup)
    1257     if collectMode == "incr":
    1258        _writeLastRevision(config, revisionPath, endRevision)
    1259     logger.info("Completed backing up Subversion repository [%s].", repository.repositoryPath)
    1260
    1261 -def _getOutputFile(backupPath, compressMode):
    1262     """
    1263     Opens the output file used for saving the Subversion dump.
    1264
    1265     If the compress mode is "gzip", we'll open a C{GzipFile}, and if the
    1266     compress mode is "bzip2", we'll open a C{BZ2File}.  Otherwise, we'll just
    1267     return an object from the normal C{open()} method.
    1268
    1269     @param backupPath: Path to file to open.
    1270     @param compressMode: Compress mode of file ("none", "gzip", "bzip").
    1271
    1272     @return: Output file object.
    1273     """
    1274     if compressMode == "gzip":
    1275        return GzipFile(backupPath, "w")
    1276     elif compressMode == "bzip2":
    1277        return BZ2File(backupPath, "w")
    1278     else:
    1279        return open(backupPath, "w")
    1280
    1281 -def _loadLastRevision(revisionPath):
    1282     """
    1283     Loads the indicated revision file from disk into an integer.
    1284
    1285     If we can't load the revision file successfully (either because it doesn't
    1286     exist or for some other reason), then a revision of -1 will be returned -
    1287     but the condition will be logged.  This way, we err on the side of backing
    1288     up too much, because anyone using this will presumably be adding 1 to the
    1289     revision, so they don't duplicate any backups.
    1290
    1291     @param revisionPath: Path to the revision file on disk.
    1292
    1293     @return: Integer representing last backed-up revision, -1 on error or if none can be read.
    1294     """
    1295     if not os.path.isfile(revisionPath):
    1296        startRevision = -1
    1297        logger.debug("Revision file [%s] does not exist on disk.", revisionPath)
    1298     else:
    1299        try:
    1300           startRevision = pickle.load(open(revisionPath, "r"))
    1301           logger.debug("Loaded revision file [%s] from disk: %d.", revisionPath, startRevision)
    1302        except:
    1303           startRevision = -1
    1304           logger.error("Failed loading revision file [%s] from disk.", revisionPath)
    1305     return startRevision
    1306
    1307 -def _writeLastRevision(config, revisionPath, endRevision):
    1308     """
    1309     Writes the end revision to the indicated revision file on disk.
    1310
    1311     If we can't write the revision file successfully for any reason, we'll log
    1312     the condition but won't throw an exception.
    1313
    1314     @param config: Config object.
    1315     @param revisionPath: Path to the revision file on disk.
    1316     @param endRevision: Last revision backed up on this run.
    1317     """
    1318     try:
    1319        pickle.dump(endRevision, open(revisionPath, "w"))
    1320        changeOwnership(revisionPath, config.options.backupUser, config.options.backupGroup)
    1321        logger.debug("Wrote new revision file [%s] to disk: %d.", revisionPath, endRevision)
    1322     except:
    1323        logger.error("Failed to write revision file [%s] to disk.", revisionPath)
    1324
    1325
    1326  ##############################
    1327  # backupRepository() function
    1328  ##############################
    1329
    1330 -def backupRepository(repositoryPath, backupFile, startRevision=None, endRevision=None):
    1331     """
    1332     Backs up an individual Subversion repository.
    1333
    1334     The starting and ending revision values control an incremental backup.  If
    1335     the starting revision is not passed in, then revision zero (the start of the
    1336     repository) is assumed.  If the ending revision is not passed in, then the
    1337     youngest revision in the database will be used as the endpoint.
    1338
    1339     The backup data will be written into the passed-in back file.  Normally,
    1340     this would be an object as returned from C{open}, but it is possible to use
    1341     something like a C{GzipFile} to write compressed output.  The caller is
    1342     responsible for closing the passed-in backup file.
    1343
    1344     @note: This function should either be run as root or as the owner of the
    1345     Subversion repository.
    1346
    1347     @note: It is apparently I{not} a good idea to interrupt this function.
    1348     Sometimes, this leaves the repository in a "wedged" state, which requires
    1349     recovery using C{svnadmin recover}.
    1350
    1351     @param repositoryPath: Path to Subversion repository to back up
    1352     @type repositoryPath: String path representing Subversion repository on disk.
    1353
    1354     @param backupFile: Python file object to use for writing backup.
    1355     @type backupFile: Python file object as from C{open()} or C{file()}.
    1356
    1357     @param startRevision: Starting repository revision to back up (for incremental backups)
    1358     @type startRevision: Integer value >= 0.
    1359
    1360     @param endRevision: Ending repository revision to back up (for incremental backups)
    1361     @type endRevision: Integer value >= 0.
    1362
    1363     @raise ValueError: If some value is missing or invalid.
    1364     @raise IOError: If there is a problem executing the Subversion dump.
    1365     """
    1366     if startRevision is None:
    1367        startRevision = 0
    1368     if endRevision is None:
    1369        endRevision = getYoungestRevision(repositoryPath)
    1370     if int(startRevision) < 0:
    1371        raise ValueError("Start revision must be >= 0.")
    1372     if int(endRevision) < 0:
    1373        raise ValueError("End revision must be >= 0.")
    1374     if startRevision > endRevision:
    1375        raise ValueError("Start revision must be <= end revision.")
    1376     args = [ "dump", "--quiet", "-r%s:%s" % (startRevision, endRevision), "--incremental", repositoryPath, ]
    1377     command = resolveCommand(SVNADMIN_COMMAND)
    1378     result = executeCommand(command, args, returnOutput=False, ignoreStderr=True, doNotLog=True, outputFile=backupFile)[0]
    1379     if result != 0:
    1380        raise IOError("Error [%d] executing Subversion dump for repository [%s]." % (result, repositoryPath))
    1381     logger.debug("Completed dumping subversion repository [%s].", repositoryPath)
    1382
    1383
    1384  #################################
    1385  # getYoungestRevision() function
    1386  #################################
    1387
    1388 -def getYoungestRevision(repositoryPath):
    1389     """
    1390     Gets the youngest (newest) revision in a Subversion repository using C{svnlook}.
    1391
    1392     @note: This function should either be run as root or as the owner of the
    1393     Subversion repository.
    1394
    1395     @param repositoryPath: Path to Subversion repository to look in.
    1396     @type repositoryPath: String path representing Subversion repository on disk.
    1397
    1398     @return: Youngest revision as an integer.
    1399
    1400     @raise ValueError: If there is a problem parsing the C{svnlook} output.
    1401     @raise IOError: If there is a problem executing the C{svnlook} command.
    1402     """
    1403     args = [ 'youngest', repositoryPath, ]
    1404     command = resolveCommand(SVNLOOK_COMMAND)
    1405     (result, output) = executeCommand(command, args, returnOutput=True, ignoreStderr=True)
    1406     if result != 0:
    1407        raise IOError("Error [%d] executing 'svnlook youngest' for repository [%s]." % (result, repositoryPath))
    1408     if len(output) != 1:
    1409        raise ValueError("Unable to parse 'svnlook youngest' output.")
    1410     return int(output[0])
    1411
    1412
    1413  ########################################################################
    1414  # Deprecated functionality
    1415  ########################################################################
    1416
    1417 -class BDBRepository(Repository):
    1418
    1419     """
    1420     Class representing Subversion BDB (Berkeley Database) repository configuration.
    1421     This object is deprecated.  Use a simple L{Repository} instead.
    1422     """
    1423
    1424 -   def __init__(self, repositoryPath=None, collectMode=None, compressMode=None):
    1425        """
    1426        Constructor for the C{BDBRepository} class.
    1427        """
    1428        super(BDBRepository, self).__init__("BDB", repositoryPath, collectMode, compressMode)
    1429
    1430 -   def __repr__(self):
    1431        """
    1432        Official string representation for class instance.
    1433        """
    1434        return "BDBRepository(%s, %s, %s)" % (self.repositoryPath, self.collectMode, self.compressMode)
    1435
    1436
    1437 -class FSFSRepository(Repository):
    1438
    1439     """
    1440     Class representing Subversion FSFS repository configuration.
    1441     This object is deprecated.  Use a simple L{Repository} instead.
    1442     """
    1443
    1444 -   def __init__(self, repositoryPath=None, collectMode=None, compressMode=None):
    1445        """
    1446        Constructor for the C{FSFSRepository} class.
    1447        """
    1448        super(FSFSRepository, self).__init__("FSFS", repositoryPath, collectMode, compressMode)
    1449
    1450 -   def __repr__(self):
    1451        """
    1452        Official string representation for class instance.
    1453        """
    1454        return "FSFSRepository(%s, %s, %s)" % (self.repositoryPath, self.collectMode, self.compressMode)
    1455
    1456
    1457 -def backupBDBRepository(repositoryPath, backupFile, startRevision=None, endRevision=None):
    1458     """
    1459     Backs up an individual Subversion BDB repository.
    1460     This function is deprecated.  Use L{backupRepository} instead.
    1461     """
    1462     return backupRepository(repositoryPath, backupFile, startRevision, endRevision)
    1463
    1464
    1465 -def backupFSFSRepository(repositoryPath, backupFile, startRevision=None, endRevision=None):
    1466     """
    1467     Backs up an individual Subversion FSFS repository.
    1468     This function is deprecated.  Use L{backupRepository} instead.
    1469     """
    1470     return backupRepository(repositoryPath, backupFile, startRevision, endRevision)
    1471