13
WORKING WITH THE CLOUD

image

One of the most significant changes in the last decade has been the rise of the internet as an appliance, and most notable is internet-based data storage. First it was used just for backups, but now with the concurrent rise of mobile technology, cloud-based storage is useful for day-to-day disk usage. Apps that use the cloud include music libraries (iCloud for iTunes) and file archives (OneDrive on Windows systems and Google Drive on Android devices).

Some systems are now completely built around the cloud. One example is Google’s Chrome operating system, a complete working environment built around a web browser. Ten years ago, that would have sounded daft, but when you think about how much time you spend in your browser nowadays ... well, no one in Cupertino or Redmond is laughing anymore.

The cloud is ripe for shell script additions, so let’s jump in. The scripts in this chapter will focus mainly on OS X, but the concepts can be easily replicated on Linux or other BSD systems.

#89 Keeping Dropbox Running

Dropbox is one of a number of useful cloud storage systems, and it’s particularly popular with people who use a variety of devices due to its wide availability across iOS, Android, OS X, Windows, and Linux. It’s important to understand that, while Dropbox is a cloud storage system, the piece that shows up on your own device is a small app designed to run in the background, connect your system to the Dropbox internet-based servers, and offer a fairly minimal user interface. Without the Dropbox application running in the background, we won’t be able to successfully back up and sync files from our computer to Dropbox.

Therefore, testing whether the program is running is a simple matter of invoking ps, as shown in Listing 13-1.

The Code

   #!/bin/bash
   # startdropbox--Makes sure Dropbox is running on OS X

   app="Dropbox.app"
   verbose=1

   running="$(ps aux | grep -i $app | grep -v grep)"

   if [ "$1" = "-s" ] ; then         # -s is for silent mode.
     verbose=0
   fi

   if [ ! -z "$running" ] ; then
     if [ $verbose -eq 1 ] ; then
       echo "$app is running with PID $(echo $running | cut -d\  -f2)"
     fi
   else
     if [ $verbose -eq 1 ] ; then
       echo "Launching $app"
     fi
   open -a $app
   fi

   exit 0

Listing 13-1: The startdropbox script

How It Works

There are two key lines in the script, denoted with and . The first invokes the ps command and then uses a sequence of grep commands to look for the specified app—Dropbox.app—and simultaneously filters itself out of the results. If the resultant string is nonzero, the Dropbox program is running and daemonized (a daemon is a program designed to run in the background 24/7 and perform useful tasks that don’t require user intervention) and we’re done.

If the Dropbox.app program isn’t running, then invoking open on OS X does the job of finding the app and launching it.

Running the Script

With the -s flag to eliminate output, there’s nothing to see. By default, however, there’s a brief status output, as Listing 13-2 shows.

The Results

$ startdropbox
Launching Dropbox.app
$ startdropbox
Dropbox.app is running with PID 22270

Listing 13-2: Running the startdropbox script to start Dropbox.app

Hacking the Script

Not much can be done with this, but if you want to get the script working on a Linux system, make sure you’ve installed the official Dropbox packages from their website. You can invoke Dropbox (once properly configured) with startdropbox.

#90 Syncing Dropbox

With a cloud-based system like Dropbox, it’s a no-brainer to write a script that lets you keep a folder or set of files in sync. Dropbox works by keeping everything in the Dropbox directory synchronized between local and cloud-based copy, typically by emulating a local hard drive on the system.

The script in Listing 13-3, syncdropbox, takes advantage of that fact by offering an easy way to copy a directory full of files or a specified set of files into the Dropbox universe. In the former instance, a copy of every file in the directory will be copied over; in the latter, a copy of every file specified will be dropped into the sync folder on Dropbox.

The Code

   #!/bin/bash
   # syncdropbox--Synchronize a set of files or a specified folder with Dropbox.
   #   This is accomplished by copying the folder into ~/Dropbox or the set of
   #   files into the sync folder in Dropbox and then launching Dropbox.app
   #   as needed.

   name="syncdropbox"
   dropbox="$HOME/Dropbox"
   sourcedir=""
   targetdir="sync"    # Target folder on Dropbox for individual files

   # Check starting arguments.

   if [ $# -eq 0 ] ; then
     echo "Usage: $0 [-d source-folder] {file, file, file}" >&2
     exit 1
   fi

   if [ "$1" = "-d" ] ; then
     sourcedir="$2"
     shift; shift
   fi

   # Validity checks

   if [ ! -z "$sourcedir" -a $# -ne 0 ] ; then
     echo "$name: You can't specify both a directory and specific files." >&2
     exit 1
   fi

   if [ ! -z "$sourcedir" ] ; then
     if [ ! -d "$sourcedir" ] ; then
       echo "$name: Please specify a source directory with -d." >&2
       exit 1
     fi
   fi

   #######################
   #### MAIN BLOCK
   #######################

   if [ ! -z "$sourcedir" ] ; then
   if [ -f "$dropbox/$sourcedir" -o -d "$dropbox/$sourcedir" ] ; then
       echo "$name: Specified source directory $sourcedir already exists." >&2
       exit 1
     fi

     echo "Copying contents of $sourcedir to $dropbox..."
     # -a does a recursive copy, preserving owner info, etc.
     cp -a "$sourcedir" $dropbox
   else
     # No source directory, so we've been given individual files.
     if [ ! -d "$dropbox/$targetdir" ] ; then
       mkdir "$dropbox/$targetdir"
       if [ $? -ne 0 ] ; then
         echo "$name: Error encountered during mkdir $dropbox/$targetdir." >&2
         exit 1
       fi
     fi
     # Ready! Let's copy the specified files.

 cp -p -v "$@" "$dropbox/$targetdir"
   fi

   # Now let's launch the Dropbox app to let it do the actual sync, if needed.
   exec startdropbox -s

Listing 13-3: The syncdropbox script

How It Works

The vast majority of Listing 13-3 is testing for error conditions, which is tedious but useful for ensuring that the script is invoked properly and isn’t going to mess anything up. (We don’t want any lost data!)

The complexity comes from the test expressions, like the one at . This tests whether the destination directory for a directory copy $sourcedir in the Dropbox folder is a file (which would be weird) or an existing directory. Read it as “if exists-as-a-file $dropbox/$sourcedir OR exists-as-a-directory $dropbox/$sourcedir, then ...”

In the other interesting line, we invoke cp to copy individually specified files. You might want to read the cp man page to see what all those flags do. Remember that $@ is a shortcut for all the positional parameters specified when the command was invoked.

Running the Script

As with many of the scripts in this book, you can invoke this without arguments to get a quick refresher in how to use it, as Listing 13-4 demonstrates.

$ syncdropbox
Usage: syncdropbox [-d source-folder] {file, file, file}

Listing 13-4: Printing the usage for the syncdropbox script

The Results

Now in Listing 13-5, let’s push a specific file to be synchronized and backed up to Dropbox.

$ syncdropbox test.html
test.html -> /Users/taylor/Dropbox/sync/test.html
$

Listing 13-5: Syncing a specific file to Dropbox

Easy enough, and helpful when you recall that this makes the specified files—or directory full of files—easily accessible from any other device that’s logged in to your Dropbox account.

Hacking the Script

When a directory is specified but already exists on Dropbox, it would be far more useful to compare the contents of the local and Dropbox directories than to just print an error and fail. Additionally, when specifying a set of files, it would be very useful to be able to specify the destination directory in the Dropbox file hierarchy.

#91 Creating Slide Shows from Cloud Photo Streams

Some people love the iCloud photo backup service Photo Stream, while others find its tendency to keep a copy of every photo taken—even the throwaway junker photographs from mobile devices—annoying. Still, it’s pretty common to sync photos with a favorite cloud backup service. The drawback is that these files are essentially hidden—because they’re buried deep in your filesystem, they won’t be automatically picked up by many photo slide show programs.

We’ll make this better with slideshow, a simple script (shown in Listing 13-6) that polls the camera upload folder and displays the pictures therein, constrained to specific dimensions. In order to achieve the desired effect, we can use the display utility that’s shipped with ImageMagick (a suite of powerful utilities you’ll learn more about in the next chapter). On OS X, the brew package manager user can install ImageMagick easily:

$ brew install imagemagick --with-x11

NOTE

A few years ago, Apple stopped shipping X11, a popular Linux and BSD graphics library, with their main operating system. In order to use the slideshow script on OS X, you’ll need to provide ImageMagick with the X11 libraries and resources that it requires by installing the XQuartz software package. You can find more information about XQuartz and how to install it on the official website: https://www.xquartz.org/.

The Code

   #!/bin/bash
   # slideshow--Displays a slide show of photos from the specified directory.
   #   Uses ImageMagick's "display" utility.

   delay=2              # Default delay in seconds
 psize="1200x900>"    # Preferred image size for display

   if [ $# -eq 0 ] ; then
     echo "Usage: $(basename $0) watch-directory" >&2
     exit 1
   fi

   watch="$1"

   if [ ! -d "$watch" ] ; then
     echo "$(basename $0): Specified directory $watch isn't a directory." >&2
     exit 1
   fi

   cd "$watch"

   if [ $? -ne 0 ] ; then
     echo "$(basename $0): Failed trying to cd into $watch" >&2
     exit 1
   fi

   suffixes="$(file * | grep image | cut -d: -f1 | rev | cut -d. -f1 | \
      rev | sort | uniq | sed 's/^/\*./')"

   if [ -z "$suffixes" ] ; then
     echo "$(basename $0): No images to display in folder $watch" >&2
     exit 1
   fi

   /bin/echo -n "Displaying $(ls $suffixes | wc -l) images from $watch "
 set -f ; echo "with suffixes $suffixes" ; set +f

   display -loop 0 -delay $delay -resize $psize -backdrop $suffixes

   exit 0

Listing 13-6: The slideshow script

How It Works

There’s not a lot to Listing 13-6 other than the painful process of figuring out each argument ImageMagick requires to make the display command perform as desired. All of Chapter 14 is about ImageMagick because the tools are so darn useful, so this is just a taste of what’s to come. For now, just trust that things are written properly, including the weird-looking image geometry of 1200x900> , where the trailing > means “resize images to fit within these dimensions while staying proportional to the original geometry.”

In other words, an image that’s 2200 × 1000 would be resized automatically to fit within the 1200-pixel wide constraint, and the vertical dimension would change proportionally from 1000 pixels to 545 pixels. Neat!

The script also ensures that there are images in the specified directory by extracting all the image files with the file command and then, through a rather gnarly pipe sequence, reducing those filenames to just their suffixes (*.jpg, *.png, and so on).

The problem with having this code in a shell script is that every time the script refers to the asterisk, it’s expanded to all the filenames that match the wildcard symbols, so it won’t display just *.jpg, but all the .jpg files in the current directory. That’s why the script temporarily disables globbing , the ability of the shell to expand these wildcards to other filenames.

However, if globbing is turned off for the entire script, the display program will complain it can’t find an image file called *.jpg. That wouldn’t be good.

Running the Script

Specify a directory that contains one or more image files, ideally a photo archive from a cloud backup system like OneDrive or Dropbox, as Listing 13-7 shows.

The Results

$ slideshow ~/SkyDrive/Pictures/
Displaying 2252 images from ~/Skydrive/Pictures/ with suffixes *.gif *.jpg *.png

Listing 13-7: Running the slideshow script to display images in a cloud archive

After running the script, a new window should pop up that will slowly cycle through your backed-up and synced images. This would be a handy script for sharing all those great vacation photos!

Hacking the Script

There’s a lot you can do to make this script more elegant, much of which is related to letting users specify the values that are currently hardcoded into the call to display (such as the picture resolution). In particular, you can allow the use of different display devices so the image can be pushed to a second screen, or you can allow the user to change the delay time between images.

#92 Syncing Files with Google Drive

Google Drive is another popular cloud-based storage system. Tied into the Google office utility suite, it turns out to be the gateway to an entire online editing and production system, which makes it doubly interesting as a sync target. Copy a Microsoft Word file onto your Google Drive, and you can subsequently edit it within any web browser, whether it’s on your computer or not. Ditto with presentations, spreadsheets, and even photographs. Darn useful!

One interesting note is that Google Drive does not store its Google Docs files on your system, but rather stores pointers to the documents in the cloud. For example, consider this:

$ cat M3\ Speaker\ Proposals\ \(voting\).gsheet
{"url": "https://docs.google.com/spreadsheet/ccc?key=0Atax7Q4SMjEzdGdxYVVzdXRQ
WVpBUFh1dFpiYlpZS3c&usp=docslist_api", "resource_id": "spreadsheet:0Atax7Q4SMj
EzdGdxYVVzdXRQWVpBUFh1dFpiYlpZS3c"}

That’s definitely not the contents of that spreadsheet.

With some fiddling with curl, you could likely write a utility to analyze this meta information, but let’s focus on something a bit easier: a script that lets you pick and choose files to have automatically mirrored on your Google Drive account, detailed in Listing 13-8.

The Code

   #!/bin/bash
   # syncgdrive--Lets you specify one or more files to automatically copy
   #   to your Google Drive folder, which syncs with your cloud account

   gdrive="$HOME/Google Drive"
   gsync="$gdrive/gsync"
   gapp="Google Drive.app"

   if [ $# -eq 0 ] ; then
     echo "Usage: $(basename $0) [file or files to sync]" >&2
     exit 1
   fi

   # First, is Google Drive running? If not, launch it.
 if [ -z "$(ps -ef | grep "$gapp" | grep -v grep)" ] ; then
     echo "Starting up Google Drive daemon..."
     open -a "$gapp"
   fi

   # Now, does the /gsync folder exist?
   if [ ! -d "$gsync" ] ; then
     mkdir "$gsync"
     if [ $? -ne 0 ] ; then
       echo "$(basename $0): Failed trying to mkdir $gsync" >&2
       exit 1
     fi
   fi

   for name  # Loop over the arguments passed to the script.
   do
     echo "Copying file $name to your Google Drive"
     cp -a "$name" "$gdrive/gsync/"
   done

   exit 0

Listing 13-8: The syncgdrive script

How It Works

Like Script #89 on page 300, this script checks whether the particular cloud service daemon is running before copying a file or files into the Google Drive folder. This is accomplished in the block of code at .

To write really clean code, we should probably check the return code from the open call, but we’ll leave that as an exercise for the reader, okay? image

After this, the script ensures the existence of a subdirectory on Google Drive called gsync, creating it if needed, and simply copies the designated file or files into it using the handy -a option to cp to ensure that the creation and modification times are retained.

Running the Script

Simply specify one or more files that you’d like to have synced up with your Google Drive account, and the script will do all the behind-the-scenes work to ensure that happens.

The Results

This is cool, actually. Specify a file you want copied to Google Drive, as Listing 13-9 shows.

$ syncgdrive sample.crontab
Starting up Google Drive daemon...
Copying file sample.crontab to your Google Drive
$ syncgdrive ~/Documents/what-to-expect-op-ed.doc
Copying file /Users/taylor/Documents/what-to-expect-op-ed.doc to your Google
Drive

Listing 13-9: Starting Google Drive and syncing files with the syncgdrive script

Notice that the first time it runs, it has to launch the Google Drive daemon, too. After you wait a few seconds for the files to be copied to the cloud storage system, they show up in the web interface to Google Drive, as shown in Figure 13-1.

image

Figure 13-1: Sample.crontab and an office document synced with Google Drive automatically show up online.

Hacking the Script

There’s a bit of false advertising here: when you specify a file to sync, the script doesn’t keep it in sync with future file changes; it just copies the file once and is done. A really interesting hack would be to create a more powerful version of this script in which you specify files you want to keep backed up and it checks them on a regular basis, copying any that are new up to the gsync directory.

#93 The Computer Says . . .

OS X includes a sophisticated voice synthesis system that can tell you what’s going on with your system. Often it’s located in the Accessibility options, but you can do a lot with a computer that can, for example, speak error messages or read files out loud.

It turns out that all of this power—and a bunch of fun voices—is also accessible from the command line in OS X, through a built-in utility called say. You can test it out with this command:

$ say "You never knew I could talk to you, did you?"

We knew you’d think it was fun!

There’s a lot you can do with the built-in program, but this is also a perfect opportunity to write a wrapper script that makes it easier to ascertain what voices are installed and get a demo of each one. The script in Listing 13-10 doesn’t replace the say command; it just makes the command easier to work with (a common theme throughout this book).

The Code

   #!/bin/bash
   # sayit--Uses the "say" command to read whatever's specified (OS X only)

   dosay="$(which say) --quality=127"
   format="$(which fmt) -w 70"

   voice=""                # Default system voice
   rate=""                 # Default to the standard speaking rate

   demovoices()
   {
     # Offer up a sample of each available voice.

   voicelist=$( say -v \? | grep "en_" | cut -c1-12 \
       | sed 's/ /_/;s/ //g;s/_$//')

     if [ "$1" = "list" ] ; then
       echo "Available voices: $(echo $voicelist | sed 's/ /, /g;s/_/ /g') \
         | $format"
       echo "HANDY TIP: use \"$(basename $0) demo\" to hear all the voices"
       exit 0
     fi

   for name in $voicelist ; do
       myname=$(echo $name | sed 's/_/ /')
       echo "Voice: $myname"
       $dosay -v "$myname" "Hello! I'm $myname. This is what I sound like."
     done

     exit 0
   }

   usage()
   {
     echo "Usage: sayit [-v voice] [-r rate] [-f file] phrase"
     echo "   or: sayit demo"
     exit 0
   }

   while getopts "df:r:v:" opt; do
     case $opt in
       d ) demovoices list    ;;
       f ) input="$OPTARG"    ;;
       r ) rate="-r $OPTARG"  ;;
       v ) voice="$OPTARG"    ;;
     esac
   done

   shift $(($OPTIND - 1))

   if [ $# -eq 0 -a -z "$input" ] ; then
     $dosay "Hey! You haven't given me any parameters to work with."
     echo "Error: no parameters specified. Specify a file or phrase."
     exit 0
   fi

   if [ "$1" = "demo" ] ; then
     demovoices
   fi

   if [ ! -z "$input" ] ; then
     $dosay $rate -v "$voice" -f $input
   else
     $dosay $rate -v "$voice" "$*"
   fi
   exit 0

Listing 13-10: The sayit script

How It Works

There are even more voices installed than are listed in the summary (those are just the ones optimized for English). To get the full list of voices, we’ll have to go back to the original say command with the -v \? parameters. What follows is an abridged version of the full list of voices:

$ say -v \?
Agnes       en_US   # Isn't it nice to have a computer that will talk to you?
Albert      en_US   # I have a frog in my throat. No, I mean a real frog!
Alex        en_US   # Most people recognize me by my voice.
Alice       it_IT   # Salve, mi chiamo Alice e sono una voce italiana.
--snip--
Zarvox      en_US   # That looks like a peaceful planet.
Zuzana      cs_CZ   # Dobrý den, jmenuji se Zuzana. Jsem český hlas.
$

Our favorite comments are for Pipe Organ (“We must rejoice in this morbid voice.”) and Zarvox (“That looks like a peaceful planet.”).

Clearly, though, this is too many voices to choose from. Plus, some of them really mangle English pronunciation. One solution would be to filter by "en_" (or by another language of your preference) to get only the English-language voices. You could use "en_US" for US English, but the other English voices are worth hearing. We get a full list the voices at .

We include the complicated sequence of sed substitutions at the end of this block because it’s not a well-formed list: there are one-word names (Fiona) and two-word names (Bad News), but spaces are also used to create the columnar data. To solve this problem, the first space in each line is converted into an underscore and all other spaces are then removed. If the voice has a single-word name, it will then look like this: "Ralph_", and the final sed substitution will remove any trailing underscores. At the end of this process, two-word names have an underscore, so they’ll need to be fixed when output to the user. However, the code has the nice side effect of making the while loop a lot easier to write with the default space-as-separator.

The other fun segment is where each voice introduces itself in sequence—the sayit demo invocation—at .

This is all quite easy, once you understand how the say command itself works.

Running the Script

Since this script produces audio, there’s not much you can see here in the book, and since we don’t yet have the audiobook of Wicked Cool Shell Scripts (can you imagine all the things you wouldn’t see?), you’ll need to do some of this yourself to experience the results. But the script’s ability to list all the installed voices can be demonstrated, as in Listing 13-11.

The Results

$ sayit -d
Available voices: Agnes, Albert, Alex, Bad News, Bahh, Bells, Boing,
Bruce, Bubbles, Cellos, Daniel, Deranged, Fred, Good News, Hysterical,
Junior, Karen, Kathy, Moira, Pipe Organ, Princess, Ralph, Samantha,
Tessa, Trinoids, Veena, Vicki, Victoria, Whisper, Zarvox
HANDY TIP: use "sayit.sh demo" to hear all the different voices
$ sayit "Yo, yo, dog! Whassup?"
$ sayit -v "Pipe Organ" -r 60 "Yo, yo, dog! Whassup?"
$ sayit -v "Ralph" -r 80 -f alice.txt

Listing 13-11: Running the sayit script to print supported voices and then speak

Hacking the Script

A close examination of the output of say -v \? reveals that there’s at least one voice where the language encoding is wrong. Fiona is listed as en-scotland, not en_scotland, which would be more consistent (given that Moira is listed as en_IE, not en-irish or en-ireland). An easy hack is to have the script work with both en_ and en-. Otherwise, dabble with it and think about when it could be useful to have a script—or daemon—talk to you.