Skip to content

Instantly share code, notes, and snippets.

@pch
Last active August 30, 2021 22:13
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save pch/58368d60926a8373d25971f58d9e3ffc to your computer and use it in GitHub Desktop.
Save pch/58368d60926a8373d25971f58d9e3ffc to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
#
# Recreate Lightroom catalog by replacing corrupt files with recovered copies
#
# Keeps the directory structure and creates the new catalog in a new path,
# which means you'll need at least 3x the size of your photos of free space.
#
# Dependencies:
# - ruby
# - exiftool
#
# See: https://ptrchm.com/blog/how-i-nearly-lost-all-my-photos/
require 'yaml'
require 'fileutils'
# Replace these:
CORRUPT_CATALOG_PATH = '/Volumes/Photos Piotr'
RECOVERED_FILES_PATH = '/Volumes/home/foo'
NEW_CATALOG_PATH = '/Volumes/Photos LR'
CATALOG_INDEX_CACHE_PATH = './catalog_index.yml'
RECOVERY_INDEX_CACHE_PATH = './recovery_index.yml'
# 1. Scan existing catalog and create index of: file_path => timestamp
# This can take a while, depending on the size of the catalog
unless File.exists?(CATALOG_INDEX_CACHE_PATH)
catalog_index = {}
puts
puts "1. Scanning existing catalog..."
Dir["#{CORRUPT_CATALOG_PATH}/**/*"].each do |filename|
next unless File.file?(filename)
puts "\t#{filename}"
timestamp = `exiftool -s -s -s -ModifyDate '#{filename}'`.strip
puts "\t\t#{timestamp}"
catalog_index[filename] = timestamp
end
puts "\n\tDone.\n\n"
File.open(CATALOG_INDEX_CACHE_PATH, "w") { |file| file.write(catalog_index.to_yaml) }
end
# 2. Scan recovered files and create index of: timestamp => file_path
# NOTE: timestamps for XMP sidecar files are the same as ARW files,
# so multiple files can be assigned to 1 timestamp
unless File.exists?(RECOVERY_INDEX_CACHE_PATH)
recovery_index = {}
puts "2. Scanning recovered files..."
Dir["#{RECOVERED_FILES_PATH}/**/*"].each do |filename|
next unless File.file?(filename)
puts "\tFile: #{filename}"
timestamp = `exiftool -s -s -s -ModifyDate '#{filename}'`.strip
puts "\t\t#{timestamp}"
next if timestamp.empty?
recovery_index[timestamp] ||= []
recovery_index[timestamp] << filename
end
puts "\n\tDone.\n\n"
File.open(RECOVERY_INDEX_CACHE_PATH, "w") { |file| file.write(recovery_index.to_yaml) }
end
# 3. Recreate catalog in a new destination
catalog_index = YAML.load(File.read(CATALOG_INDEX_CACHE_PATH))
recovery_index = YAML.load(File.read(RECOVERY_INDEX_CACHE_PATH))
missing_files = []
catalog_index.each do |filename, timestamp|
next if timestamp.empty?
recovered = false
print "Recovering file: #{filename} | #{timestamp}... "
path = File.dirname(filename)
basename = File.basename(filename)
extname = File.extname(filename)
new_path = path.gsub(CORRUPT_CATALOG_PATH, NEW_CATALOG_PATH)
# Recreate directory in a new path
FileUtils.mkdir_p(new_path)
# Find the corresponding recovered file and copy it to the new catalog
if recovery_index[timestamp]
# Compare extensions in case multiple files are stored under the same timestamp
recovery_index[timestamp].each do |recovered_filename|
next if extname.downcase != File.extname(recovered_filename).downcase
FileUtils.cp(recovered_filename, "#{new_path}/#{basename}")
recovered = true
print "OK"
break
end
end
puts
unless recovered
puts "\tFile not recovered...\n\n"
missing_files << filename
end
end
puts "ALL DONE!"
puts "\tFiles to recover: #{catalog_index.keys.count}"
puts "\tMissing files: #{missing_files.count}"
missing_files.each do |missing|
puts "\t\t#{missing}"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment