Last active
August 30, 2021 22:13
-
-
Save pch/58368d60926a8373d25971f58d9e3ffc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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