Originally published 2017-09-18. This no longer works as of 2019-07-10, when Google removed automatic syncing between Google Photos and Google Drive. As I'm no longer using Google Photos as my primary photo storage service, I won't be developing a new solution. The information below may still be useful if you want to sync between iCloud photos and some folder on you computer.
I want to sync photos through both Google and iCloud. If you’re taking the pictures on an iOS device, this is simple - you just ensure iCloud sync is enabled and also that you’re signed in to the Google Photos app. If you’re pushing to Google Photos from another source, there’s not an obvious way to automatically sync back to iCloud. What follows is a rough way to accomplish it using a Mac as an intermediate device.
Google Photos can be configured to sync all photo/video files to a folder on Google Drive. The Google Drive app (now called Backup & Sync) can be used to ensure that syncs to a folder on the Mac. With that enabled, we just need a script to periodically check for files in the Google Drive folder that are missing from the Photos app on the Mac, then import them.
The Photos app stores all its media files in the directory ~/Pictures/Photos Library.photoslibrary/Masters/
and its subdirectories. To figure out what needs to be imported, we can compare MD5 sums of all the files there with MD5 sums of all the files in the Google Drive folder, and see which ones are missing. We can then import those files using AppleScript.
At the bottom of this post is a script to do that (it’s not actually specific to Google Photos, it can be used for importing from any folder). Update the two folder paths to be correct for your computer.
To make that script run automatically every three hours, you can put the following launchd config into ~/Library/LaunchAgents/
and then either reboot, or run:
launchctl load ~/Library/LaunchAgents/net.brokensandals.photos.sync.applefromgoogle.plist
(First make sure to update all the paths in it to be correct for your computer.)
Note: the script takes several minutes to run on my library of only three or four thousand photos. The obvious next step would be to write something that gets notified on every change to Google Photos’s folder, and keeps a persistent set of MD5 sums for files in the Photos app’s folder, so that imports can be done promptly rather than waiting for an infrequent sync.
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>Label</key> | |
<string>net.brokensandals.photos.sync.applefromgoogle</string> | |
<key>Program</key> | |
<string>/Users/jacob/dotfiles/bin/sync-apple-photos-from-google-photos.rb</string> | |
<key>StartCalendarInterval</key> | |
<array> | |
<dict> | |
<key>Hour</key> | |
<integer>0</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>3</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>6</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>9</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>12</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>15</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>18</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>21</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
</array> | |
<key>StandardOutPath</key> | |
<string>/Users/jacob/log/sync-apple-photos-from-google-photos-stdout.log</string> | |
<key>StandardErrorPath</key> | |
<string>/Users/jacob/log/sync-apple-photos-from-google-photos-stderr.log</string> | |
</dict> | |
</plist> |
#!/usr/bin/env ruby | |
require 'digest' | |
require 'open3' | |
google_photos_dir = '/Users/jacob/Google Drive/Google Photos' | |
apple_photos_dir = '/Users/jacob/Pictures/Photos Library.photoslibrary/Masters' | |
def get_photo_paths(dir) | |
Dir.glob(File.join(dir, '**', '*.*')) | |
end | |
def get_photo_paths_by_digest(dir) | |
get_photo_paths(dir).group_by { |path| Digest::MD5.file(path).hexdigest } | |
end | |
google_photos = get_photo_paths_by_digest(google_photos_dir) | |
puts "Google Photos Count: #{google_photos.count}" | |
apple_photos = get_photo_paths_by_digest(apple_photos_dir) | |
puts "Apple Photos Count: #{apple_photos.count}" | |
missing_from_apple = google_photos.keys - apple_photos.keys | |
unless missing_from_apple.count > 0 | |
puts "No photos missing from Apple Photos." | |
exit 0 | |
end | |
puts "Apple Photos is missing #{missing_from_apple.count} photos:" | |
missing_from_apple.each { |digest| puts google_photos[digest].first } | |
if missing_from_apple.count > 100 && ARGV[0] != '--force' | |
STDERR.puts "There are a large number of photos to import. If you are sure this is correct, rerun with --force" | |
system 'osascript', '-e', "display notification \"Skipped sync for #{missing_from_apple.count} photos; script must be run with --force to import this many\" with title \"Google->Apple Photos Sync\"" | |
exit 1 | |
end | |
add_file_script_lines = missing_from_apple.map do |digest| | |
"set end of filesToImport to POSIX file \"#{google_photos[digest].first}\"" | |
end | |
script = <<-APPLESCRIPT | |
set filesToImport to {} | |
#{add_file_script_lines.join("\n")} | |
with timeout of 30 * 60 seconds | |
tell application "Photos" | |
import filesToImport | |
end tell | |
end timeout | |
APPLESCRIPT | |
puts "Running applescript to import photos..." | |
script_stdout, script_stderr, script_status = Open3.capture3('osascript', stdin_data: script) | |
puts script_stdout unless script_stdout.empty? | |
STDERR.puts script_stderr unless script_stderr.empty? | |
puts "Finished with exit code #{script_status}" | |
unless script_status == 0 | |
system 'osascript', '-e', "display notification \"Failed sync for up to #{missing_from_apple.count} photos\" with title \"Google->Apple Photos Sync\"" | |
exit 1 | |
end |