Backing Up to Versioned Filenames

Problem

You want to copy a file to a numbered backup before overwriting the original file. More generally: rather than overwriting an existing file, you want to use a new file whose name is based on the original filename.

Solution

Use String#succ to generate versioned suffixes for a filename until you find one that doesn't already exist:

	class File
	 def File.versioned_filename(base, first_suffix='.0')
	 suffix = nil
	 filename = base
	 while File.exists?(filename)
	 suffix = (suffix ? suffix.succ : first_suffix)
	 filename = base + suffix
	 end
	 return filename
	 end
	end
	
	5.times do |i|
	 name = File.versioned_filename('filename.txt')
	 open(name, 'w') { |f| f << "Contents for run #{i}" }
	 puts "Created #{name}"
	end
	# Created filename.txt
	# Created filename.txt.0
	# Created filename.txt.1
	# Created filename.txt.2
	# Created filename.txt.3

If you want to copy or move the original file to the versioned filename as a prelude to writing to the original file, include the ftools library to add the class methods File. copy and File.move. Then call versioned_filename and use File.copy or File.move to put the old file in its new place:

	require 'ftools'
	class File
	def File.to_backup(filename, move=false)
	 new_filename = nil
	 if File.exists? filename
	 new_filename = File. 
versioned_filename(filename)
	 File.send(move ? :move : :copy, filename, new_filename)
	 end
 	 return new_filename
	 end
	end

Let's back up filename.txt a couple of times. Recall from earlier that the files filename.txt.[0-3] already exist.

	 
	 File.to_backup('filename.txt') # => "filename.txt.4"
	 File.to_backup('filename.txt') # => "filename.txt.5"

Now let's do a destructive backup:

	File.to_backup('filename.txt', true) # => "filename.txt.6"
	File.exists? 'filename.txt' # => false

You can't back up what doesn't exist:

	File.to_backup('filename.txt') # => nil

 

Discussion

If you anticipate more than 10 versions of a file, you should add additional zeroes to the initial suffix. Otherwise, filename.txt.10 will sort before filename.txt.2 in a directory listing. A commonly used suffix is ".000".

	200.times do |i|
	 name = File.versioned_filename('many_versions.txt', '.000')
	 open(name, 'w') { |f| f << "Contents for run #{i}" }
	 puts "Created #{name}"
	end
	# Created many_versions.txt
	# Created many_versions.txt.000
	# Created many_versions.txt.001
	# …
	# Created many_versions.txt.197
	# Created many_versions.txt.198

The result of versioned_filename won't be trustworthy if other threads or processes on your machine might be trying to write the same file. If this is a concern for you, you shouldn't be satisfied with a negative result from File.exists?. In the time it takes to open that file, some other process or thread might open it before you. Once you find a file that doesn't exist, you must get an exclusive lock on the file before you can be totally certain it's okay to use.

Here's how such an implementation might look on a Unix system. The versioned_filename methods return the name of a file, but this implementation needs to return the actual file, opened and locked. This is the only way to avoid a race condition between the time the method returns a filename, and the time you open and lock the file.

	 
class File
	 def File. 
versioned_file( 
base, first_suffix='.0', access_mode='w')
	 suffix = file = locked = nil
	 filename = base
	 begin
	 suffix = (suffix ? suffix.succ : first_suffix)
	 filename = base + suffix
	 unless File.exists? filename
	 file = open(filename, access_mode)
	 locked = file.flock(File::LOCK_EX | File::LOCK_NB)
	 file.close unless locked
	 end
		 end until locked
	 return file
	 end
	end
	
	File.versioned_file('contested_file') # => #
	File.versioned_file('contested_file') # => #
	File.versioned_file('contested_file') # => #

The construct begin…end until locked creates a loop that runs at least once, and continues to run until the variable locked becomes true, indicating that a file has been opened and successfully locked.

See Also

  • Recipe 6.13, "Locking a File"


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net