If you work on Mac OS X, you may have noticed how cool Macs deal with complex documents, things like Keynote presentations or applications themselves. They're stored as directories. The Finder hides this, making them look and act like individual files. This works nicely, often the contents of a bundle are simple text and XML files ... whereas the equivalent under Windows is either a very proprietary (and potentially fragile) binary format, or multiple files and folders that YOU have to treat as a unit.
Alas, this all breaks down when using Subversion. You can't just check in MyPresentation.key into SVN ... it will create those pesky .svn directories inside the bundle, and those will be destroyed every time you save your presentation.
My solution to this is to convert the bundles into an archive, and check in the archive. The bundle folders are marked as svn:ignore. I guess this reveals that I mostly use SVN as a safe, structured backup.
In any case, manually creating those archives can be a pain. So ... out comes my solution to many problems: Ruby.
The goal here is to find bundles that need to be archived; do it efficiently (only update the archive if necessary) and do it recursively, seeking out bundles in sub-directories.
#!/usr/bin/ruby
# Used to prepare a directory for commit to Subversion. This is necessary for certain file types on Mac OS X because what appear to be files in the Finder
# are actually directories (Mac uses the term "bundle" for this concept). It is useless to put the .svn folder inside such a directory, because it will
# tend to be deleted whenever the "file" is saved.
#
# Instead, we want to compress the directory to a single archive file; the bundle will be marked as svn:ignore.
#
# We use tar with Bzip2 compression, which is resource intensive to create, but
# compresses much better than GZip or PKZip.
#
# The trick is that we only want to create the acrhive version when necessary; when
# the archive does not exist, or when any file
# in the bundle is newer than the archive.
require 'optparse'
# Set via command line options
$extensions = %w{pages key oo3 graffle}
$recursive = true
$dry_run = false
# Queue of folders to search (for bundles)
$queue = []
def matching_extension(name)
dotx = name.rindex('.')
return false unless dotx
ext = name[dotx + 1 .. -1]
return $extensions.include?(ext)
end
# Iterate over the directory, identify bundles that may need to be compressed and (if recursive) subdirectories
# to search.
#
# path: string path for a directory
def search_directory(dirpath)
Dir.foreach(dirpath) do |name|
# Skip hidden files and directories
next if name[0..0] == "."
path = File.join(dirpath, name)
next unless File.directory?(path)
if matching_extension(name)
update_archive path
next
end
if $recursive
$queue << path
end
end
end
def needs_update(bundle_path, archive_path)
return true unless File.exists?(archive_path)
archive_mtime = File.mtime(archive_path)
# The archive exists ... can we find a file inside the bundle thats newer?
# This won't catch deletions, but that's ok. Bundles tend to get completly
# overwritten when any tiny thing changes.
dirqueue = [bundle_path]
until dirqueue.empty?
dirpath = dirqueue.pop
Dir.foreach(dirpath) do |name|
path = File.join(dirpath, name)
if File.directory?(path)
dirqueue << path unless [".", ".."].include?(name)
next
end
# Is this file newer?
if File.mtime(path) > archive_mtime
return true
end
end
end
return false
end
def update_archive(path)
archive = path + ".tar.bz2"
return unless needs_update(path, archive)
if $dry_run
puts "Would create #{archive}"
return
end
puts "Creating #{archive}"
dir = File.dirname(path)
bundle = File.basename(path)
# Could probably fork and do it in a subshell
system "tar --create --file=#{archive} --bzip2 --directory=#{dir} #{bundle}"
end
$opts = OptionParser.new do |opts|
opts.banner = "Usage: prepsvn [options]"
opts.on("-d", "--dir DIR", "Add directory to search (if no directory specify, current directory is searched)") do |value|
$queue << value
end
opts.on("-e", "--extension EXTENSION", "Add another extension to match when searching for bundles to archive") do |value|
$extensions << value
end
opts.on("-N", "--non-recursive", "Do not search non-bundle sub directories for files to archive") do
$recursive = false
end
opts.on("-D", "--dry-run", "Identify what archives would be created") do
$dry_run = true
end
opts.on("-h", "--help", "Help for this command") do
puts opts
exit
end
end
def fail(message)
puts "Error: #{message}"
puts $opts
end
begin
$opts.parse!
rescue OptionParser::InvalidOption
fail $!
end
# If no --dir specified, use the current directory.
if $queue.empty?
$queue << Dir.getwd
end
until $queue.empty?
search_directory $queue.pop
end
I do love Ruby syntax, it is so minimal, and lets me follow my personal mantra less is more.
I'm sure there's some edge cases that aren't handle well, such as spaces in path names and maybe issues related to permissions. But it works for me.
You do need to have tar installed, in order to build the archives. I can't remember if that's built in to Mac OS X (probably) or whether I obtained it using Fink.
In any case, you need to remember to execute prepsvn in your workspace, to spot file bundles that need archiving, before you check in. It would be awesome if Subversion supported some client-side check-in hooks to do this automatically.