Generational backups of single file or directory, preserving extension


/ Published in: Python
Save to your folder(s)

Creates backups of "file.ext" in the form "file.Bx.ext", where Bx represents a sequential backup number from 1 (most recent) to 5 (oldest)

Put the following symbolic links in your path

ln -s path-to-this-file/age.py age
ln -s path-to-this-file/age.py sage
ln -s path-to-this-file/age.py unage

Save the code as "age.py".


Copy this code and paste it in your HTML
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. #
  4. # age.py
  5. #
  6. # Copyright 2009 Simon Tite<[email protected]>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program; if not, write to the Free Software
  20. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  21. # MA 02110-1301, USA.
  22. """Usage examples:
  23. age file.ext
  24. file.B4.ext replaces file.B5.ext
  25. file.B3.ext replaces file.B4.ext
  26. file.B2.ext replaces file.B3.ext
  27. file.B1.ext replaces file.B2.ext
  28. file.ext replaces file.B1.ext
  29. file.ext no longer exists.
  30. sage file.ext
  31. file.B4.ext replaces file.B5.ext
  32. file.B3.ext replaces file.B4.ext
  33. file.B2.ext replaces file.B3.ext
  34. file.B1.ext replaces file.B2.ext
  35. file.ext replaces file.B1.ext
  36. file.ext is preserved unchanged.
  37. unage file.ext
  38. file.B5.ext replaces file.B4.ext
  39. file.B4.ext replaces file.B3.ext
  40. file.B3.ext replaces file.B2.ext
  41. file.B2.ext replaces file.B1.ext
  42. file.B1.ext replaces file.ext
  43. file.B5.ext no longer exists.
  44. (the files age, sage and unage must be symbolic links to age.py)
  45. """
  46.  
  47. import os.path
  48. import shutil
  49. import sys
  50.  
  51. def age_one_file(filename, generation=0, infix="B", *args):
  52. """Age one file, by moving/copying it to the next generation number. The direction
  53. of the aging is defined by the presence or absence of "unage" in *args.
  54.  
  55. filename contains the original unmodified file name, eg "myfile.ext"
  56. generation the generation of this file to be aged, 0 being the original.
  57. infix the backup infix - see examples:
  58.  
  59. age_one_file("myfile.txt") copies/renames "myfile.txt" to "myfile.B1.txt"
  60. age_one_file("myfile.txt",1) copies/renames "myfile.B1.txt" to "myfile.B2.txt"
  61. age_one_file("myfile.txt",2) copies/renames "myfile.B2.txt" to "myfile.B3.txt"
  62. (In all these cases, the infix is "B".)
  63.  
  64. Any file copied to will be overwritten without warning, and if the source file does
  65. not exist, nothing will happen at all. Oher exceptions, and in particular those
  66. caused by file permission rights, are not trapped.
  67.  
  68. Optional arguments are:
  69. "verbose" Display verification of the rename
  70. "static" When the generation is 0, the original file is copied to
  71. generation 1, leaving the original file intact. For all other
  72. generations, this keyword is ignored.
  73. "unage" Roll back the generations
  74.  
  75. """
  76. verbose = "verbose" in args
  77. static = ("static" in args) and generation == 0
  78. unage = ("unage" in args)
  79. if unage:
  80. direction=-1
  81. else:
  82. direction=+1
  83. source = get_backup_name(filename, generation=generation, infix=infix)
  84. if os.path.exists(source):
  85. target = get_backup_name(filename, generation=generation + direction, infix=infix)
  86. if verbose:
  87. print "Doing " + sys.argv[0] + " " + source + " to " + target + "..."
  88. if os.path.isdir(filename):
  89.  
  90. #20091115: copytree now objects if target exists, so delete it first
  91. shutil.rmtree(target,True)
  92.  
  93. shutil.copytree(source,target,symlinks=True)
  94. if not static:
  95. shutil.rmtree(source,True)
  96. #True means ignore all errors when deleting the source directory.
  97. else:
  98. if static:
  99. shutil.copy2(source, target)
  100. else:
  101. shutil.move(source, target)
  102. if verbose:
  103. print "Done.\n"
  104. return
  105.  
  106. def get_backup_name(filename, generation=0, infix="B"):
  107. """Return the backup file name based on a generation number.
  108.  
  109. Examples:
  110. get_backup_name("myfile.txt") returns ("myfile.txt")
  111. get_backup_name("myfile.txt",1) returns ("myfile.B1.txt")
  112. get_backup_name("myfile.txt",2) returns ("myfile.B2.txt")
  113. ...and so on.
  114.  
  115. """
  116. base, ext = splitext(filename)
  117. if generation == 0:
  118. return filename
  119. else:
  120. return base + "." + infix + str(generation) + ext
  121.  
  122. def splitext(filename):
  123. #This is a simple alias of os.path.splitext, but is done this way to allow possible
  124. #alternative splits which will probably never be implemented.
  125. """Split a string (usually a filename) returning (base,ext).
  126.  
  127. Returns a 2-tuple of (base,ext) where:
  128. base = all the text to the left of the rightmost dot in the string
  129. ext = the rightmost dot in the string and all text following it
  130. The os module will handle leading dots sensibly, so that a file called (for example)
  131. ".myprofile" will be returned as (".myprofile","").
  132.  
  133. """
  134. return (os.path.splitext(filename))
  135.  
  136. def main():
  137. total_generations = 5
  138. #Find out if this was called with "age" "sage" or "unage":
  139. calledby = splitext(os.path.basename(sys.argv[0]))[0]
  140. #(take off the filename directory prefixes and extension, if any)
  141. filename = sys.argv[1] #the first argument is the file to age/unage
  142. if (calledby == "age") or (calledby == "sage"):
  143. #Symbolic links won't be handled, because I'm not sure how they should behave anyway.
  144. if os.path.islink(filename):
  145. print "Symbolic link " + filename + " cannot be aged or unaged."
  146. return -1
  147. for generation in reversed(range(total_generations)):
  148. #(start at 4, count down to zero)
  149. if calledby =="age":
  150. age_one_file(filename,generation,"B","verbose")
  151. else:
  152. age_one_file(filename,generation,"B","verbose","static")
  153. elif calledby == "unage":
  154. for generation in range(1,total_generations+1):
  155. #(start at 1, count up to 5
  156. age_one_file(filename,generation,"B","verbose","unage")
  157. else:
  158. print "Called by unknown command: use age, sage or unage."
  159. return -1
  160. return 0
  161.  
  162. if __name__ == '__main__': main()

Report this snippet


Comments

RSS Icon Subscribe to comments

You need to login to post a comment.