CedarBackup3.extend.subversion

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

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