Sunday, August 26

Automated on-demand backups with OS X

Update #2: Although the solution below is free and easy and relatively reliable, I eventually broke down and just purchased SuperDuper.


Update #1: In my haste, I forgot some important tidbits of information, such as where you put all these files...


I just [finally] got myself an external USB hard drive (Western Digital 160GB Passport) and I wanted to set up my Macbook Pro so that whenever I connected the drive to the laptop, the laptop would automatically back itself up, then give me a report of the backup and unmount the drive so I could immediately unplug it and put it away. That's how lazy I want to be about backing up.

So I scoured the 'net and finally figured out how to "git 'er done" just the way I like, thanks primarily to this old post. I'm posting this primarily for my own benefit because I know I'll have to do this again in the near future and I won't remember how I did it. So here's how it goes...

First of all you need to write yourself a fancy backup script (most comments are from the original author, not me) and place it in "~/Library/Scripts". I named my script "backup.com" to conform to the example in the original article:

#!/bin/bash

# Borrowed (and BASH'd) from...
# http://www.macresearch.org/tutorial_backups_with_launchd

# Convenience variables to specify what I want to
# backup and where I want to back it up to

HOME="/Users/ted"
VOLUME="/Volumes/WD Passport"
DEST="$VOLUME/"

# This sleep timer has been added to allow enough
# time for the system to mount the external drive
# On my PowerBook 30 sec. is more than enough time

sleep 20

# This check is added to test for cases when we are
# removing a drive from /Volumes or if the drive failed
# to mount in the first place

if [ ! -e "$VOLUME" ]; then
echo $VOLUME not mounted;
exit 0;
fi

# Create the folder to back up the data to (defined above)

if [ ! -e "$DEST" ]; then
echo $DEST does not exist;
exit 0;
fi

# Copy the files over. Here we are using rsync.

EXCLUDES="$HOME/.rsync-excludes"
RESULTS="$HOME/rsync-results.txt"

rsync --verbose --archive --modify-window=1 --exclude-from="$EXCLUDES" "$HOME" "$DEST" > "$RESULTS"
# --delete --delete-excluded

# Optionally, once the rsync is done, unmount the drive.

hdiutil detach "$VOLUME" >> "$RESULTS"

# Display the results

mate $RESULTS

exit 0

Then you need yourself a "plist" file to tell OS X's "launch control" [launchctl] service to (a) watch for the drive to be mounted [plugged in], and (b) run the aforementioned script when it is. This file goes in "~/Library/LaunchAgents" and in my case is named "net.rudiment.backup":

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" \
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<dict>
<key>Label</key>
<string>net.rudiment.backup</string>
<key>LowPriorityIO</key>
<true/>
<key>Program</key>
<string>/Users/ted/Library/Scripts/backup.com</string>
<key>ProgramArguments</key>
<array>
<string>backup.com</string>
</array>
<key>WatchPaths</key>
<array>
<string>/Volumes</string>
</array>
</dict>
</plist>

Now as the original article mentions, all you have to do is log out and log back in for this trigger to activate, but you can do it manually (and I did) like so:

Teflon-Ted:~ ted$ launchctl list
Teflon-Ted:~ ted$ launchctl load ~/Library/LaunchAgents
Teflon-Ted:~ ted$ launchctl list
net.rudiment.backup

That's it! You're golden.

And also for the sake of posterity, here's the contents of the "exclude" file referenced in the script:

.Trash/
.netbeans/
.netbeans-derby/
.gimp-2.2/
.thumbnails/
.svn/
Library/Caches/
Library/Mail/
Library/Logs/
.history/
iPod Photo Cache/
Printers/

Saturday, May 26

I gave up on referential integrity

I did the unthinkable last week. I gave up on referential integrity with my latest project. It's simply too painful to support in Ruby on Rails.

When I started the project, the first plug-in I installed was Simon Harris's Foreign Key Migrations. This plug-in rocks!

So things were cooking along and I brought on another developer. One night I got home and went through my pre-coding ritual: Get the latest code from Subversion, migrate the database, rake the tests to make sure I'm starting with a solid foundation.

Two of the tests failed.

I was livid.

How dare my colleague commit broken code to the trunk!

We started a back and forth and we eventually discovered that our databases didn't match. He was missing foreign keys that I had, and vice versa. Not good.

I shot Simon and e-mail informing him of the assumed bug and asked if he'd seen it before and had any suggestions. It was news to him. He was incredibly receptive and helpful. He asked for as much information as I could give and I sent him my entire migration set along with all the plug-ins we were using.

Simon discovered that the Engines plug-in was breaking his foreign key plug-in.

As an aside, I had worked with engines in the early days and the experience was so traumatic I swore off them for life. But my colleague had a plug-in that required them and we needed it so I caved... and shot myself in the other foot.

I thought I had fixed the problem by hacking it so that the foreign key plug-in loaded before the engines plug-in. After a fresh migration all the tests passed. But it was temporary. A few dev cycles later we found another database inconsistency.

So in the wee hours of the morning at the second or third day of Railsconf I sat outside the ballroom and wrote unit tests with fixtures for every single foreign key in the database. That's when things really started going down the tubes.

The testing framework in Rails isn't clever enough to load the fixtures in the correct order to avoid violating foreign key constraints. Ditto for the unloading between each test. It was carnage.

So I'd had it. I was fed up. I made possibly the hardest and most painful architectural decision of my software development career... I gave up on referential integrity. I removed the foreign key plug-in from trunk and committed a migration to remove all the legacy constraints. Then, I wept.

One of my biggest pet peeves is fighting the tools. Yeah it's fun to hack and extend the tools with plug-ins and monkey patches but at the end of the day if the tools are working against you instead of with you, something has to give, and in this case I think the pros of productivity, support, and community with Rails outweighed the cons of having my cake and eating it to, so I gave up. I hope I don't live to regret it.

Saturday, May 19

Railsconf Celebrity Sightings

On the way back to my hotel room for the dinner break I shared an elevator ride with Geoffrey Grosenbach (topfunky) of the Ruby on Rails Podcast and perhaps more popular PeepCode screencasts. He wasn't hard to miss with his shirt and blinged-out messenger bag both adorned with the PeepCode logo, and there was no mistaking that signature voice.

At the keynote I noticed the guy sitting next to me was playing NetHack on his laptop with the account name "rbates" and I asked if he happened to be Ryan Bates of the phenomenal Railscasts screencasts. He was.

Saturday, April 28

Grounded in Rails

If you're a Ruby on Rails fan boy, like me, you owe it to yourself to hear the voice of reason that is Dave Fayram on the latest episode of the Drunk and Retired podcast.

Tuesday, March 20

Immutable ActiveRecord Attributes

At last night's Ruby Meetup I posed the question of how to make an ActiveRecord attribute immutable, meaning the ability to set a value once and never allow it to change.

I got lots of theories but no solid answers. One person suggested using callbacks but the hooks aren't there for the logic he proposed. Another person suggested overwriting the assignment method, but I wondered about the update_attributes method undermining it. It turns out the latter calls the former so it's safe.

I wrote some unit tests today and started experimenting and found a solution that seems to work nicely (and consistently).

class Sender < ActiveRecord::Base
def email=(address)
if new_record?
write_attribute(:email, address)
else
raise 'Sender.email is immutable!'
end
end
end