flatten.rb |
So for the past 10 years I've been using the various Apple apps to manage my photo library. I've gone from iPhoto to Aperture to and now I'm trying to move to Photos. But my photo collection has become this nasty spaghetti monster that certainly contains a few duplicates and duplicates of duplicates of duplicates. The common directory structure within an Apple Photo Library is within the Library contents folder -> Masters -> Year -> Month -> then some long indexing folder name that includes a combo of numbers. It's something like this:
I have a few of these libraries from various computers over time and a few folders that were organized in different ways. My end goal is to just get all the photos into folders by just year for now. Like this:
There may be thousands of photos in one directory. Maybe I'll decide to break it down more than that someday, but there are simply too many directories and too many photos for me to go through and move them all and consolidate them one folder at a time.
All the bash one liners I found didn't work or didn't have the desired effect, so I went with a good ol' ruby script to accomplish this. The script isn't all that special. It doesn't go through and delete duplicates, or actually sort them by year or anything, but it will collapse an existing directory tree down to the root by moving all the files in subdirectories down to the root.
"Move all the files from the tree starting at source_dir"
"into dest_dir out of the tree"
# directory listings to skip
$excludes = ['.', '..', '.DS_Store']
# max file rename attempts
$max_attempts = 20
# initial directory tree depth
$depth = 0
# max allowed directory tree depth
$max_depth = 10
def flatten(source_dir, dest_dir)
# increment the tree depth
$depth += 1
# if surpassed max allowed tree depth, exit the directory
if $depth > $max_depth
puts "\n\n-- Max folder depth reached in '#{source_dir}' --\n\n"
$depth -= 1 # exit this directory
return
end
# ensure dir paths end with '/'
source_dir += "/" if source_dir[-1] != "/"
dest_dir += "/" if dest_dir[-1] != "/"
# read the items of the directory
puts "Flattening contents of '#{source_dir}' to '#{dest_dir}'\n"
Dir.entries(source_dir).each do |item|
next if $excludes.include?(item) # skip excludes
item = source_dir + item # realize full path
if File.directory?(item) # subdirectory
flatten(item, dest_dir) # recursively flatten subdir
else #file
rename_file(item, dest_dir, 0) # attempt to rename the file
end
end
# check if the directory is empty, if so delete it
# special case, ruby won't consider a dir empty if it contains .DS_Store
# delete .DS_Store first if dir otherwise empty
# also don't delete the directory if it's the root directory
begin
if (Dir.entries(source_dir) - $excludes).empty? and $depth > 1
puts "deleting #{source_dir}"
if File.file?("#{source_dir}.DS_Store")
_ = `rm "#{source_dir}.DS_Store"`
end
Dir.delete(source_dir)
end
rescue # do nothing
end
$depth -= 1 # exit this directory
end
# renames the file, moving it to the dest_dir if the file
# isn't already in the directory at dest_dir
def rename_file(filename, dest_dir, version)
# file already in dest_dir?
if File.dirname(filename) + "/" == dest_dir
puts "file #{filename} already in #{dest_dir}"
return
end
# get the straight filename, no path
fname = File.basename(filename)
# set what the new full path ought to be
new_path = dest_dir + fname
# if we are attempting new versions append the version to the basename before the extension
if version > 0
new_path = dest_dir + File.basename(filename, ".*") + "_" + version.to_s + File.extname(filename)
end
# if the new path already exists, try again
if File.file?( new_path )
puts("File '#{new_path}' exists..")
if (version > $max_attempts)
puts "Max rename attempts reached for '#{filename}'"
end
rename_file(filename, dest_dir, version += 1)
else
puts("moving '#{filename}' to '#{new_path}'")
File.rename(filename, new_path)
end
end
# START
# source dir is first argument, otherwise current directory
source_dir = ARGV.shift
# default source dir is current dir
if (source_dir == nil)
source_dir = "."
end
#destination is second argument, otherwise same as source
dest_dir = ARGV.shift
# default destination dir = source dir
if (dest_dir == nil)
dest_dir = source_dir
end
# begin the flattening process
flatten(source_dir, dest_dir)
It will keep in mind that file names may end up being the same, it will attempt to append a version number to the end of the file name. You can customize how many times it should try by changing the variable $max_attempts to the desired number. It will also only delve a certain depth into the tree based on the variable $max_depth.
To execute a ruby script you need to have ruby installed and you will have ruby installed if you install Apple Command Line Tools. To see if you have ruby installed, launch Terminal and type (exclude the '$', that signifies the bash prompt):
To execute a ruby script you need to have ruby installed and you will have ruby installed if you install Apple Command Line Tools. To see if you have ruby installed, launch Terminal and type (exclude the '$', that signifies the bash prompt):
$ which ruby
It should return a path like:
/Users/stout/.rvm/rubies/ruby-2.2.1/bin/ruby
or:
/usr/bin/ruby
If so, you're ready to rock. If not, just type in:
$ ruby
And you should get a prompt to install the Apple Command Line tools.
Now, once you have ruby installed you can execute the script and free yourself from 20,000 leagues under the directory tree by running:
Now, once you have ruby installed you can execute the script and free yourself from 20,000 leagues under the directory tree by running:
$ ruby flatten.rb source_dir dest_dir
Where flatten.rb is the name of the ruby file (this will have to be the full path to the file) source_dir is the source directory and dest_dir is the destination directory to where you want all the files to go. If you leave off the destination directory, it will just flatten the source directory to itself. If you leave off the source directory, it will assume the current directory.
Hopefully someone finds it useful.
Hopefully someone finds it useful.