Apache + AppleScript

The Purpose of this Article is Twofold...

You’re probably asking yourself exactly what Apache and AppleScript have in common. The answer? Nothing, really — the former is the web server that powers most of the internet and runs on pretty much any modern operating system and hardware platform, while the latter is a scripting language whose sole purpose is to automate repetitive tasks in the Mac OS. The only thing that ties the two together is the fact that both are included in OS X.

AppleScript traditionally interacts with Mac applications such as FileMaker, the Office suite, and countless other scriptable applications. However, it can also invoke UNIX utilities through its do shell script command, which opens up a whole new can of worms for scripters to play with. This article will take a closer look at a few of these worms and exactly how they tie Apache and AppleScript together.

OS X: Server and Client Rolled Into One

OS X has united the two (traditionally distinct) sides of computing — the server–side and client–side — under one operating system. With its *BSD foundation and Macintosh interface, OS X is equally capable when crunching numbers on a G5 cluster node as it is when installed on an iBook for editing video, touching up images, or surfing the web and sending/receiving email. OS X is by no means the first operating system to allow this convergence, but it is the first to do it in such an elegant manner. Those who don’t need the command–line will never have to look at a Terminal window, and those who work with shell scripts and pipes may never need the Finder.

One key component of OS X’s server capabilities is the Apache web server; and while it’s included in the installation, most non–technical end–users probably don’t even know that it can be toggled on and off in System Preferences.

Starting and stopping Apache is simply a matter of clicking a button in OS X

Figure #1: Starting and stopping Apache is simply a matter of clicking a button in OS X.

AppleScript, however, is a technology that has never been used on the server–side for a variety of reasons, the most obvious being the fact that it wasn’t even available on a true server platform until the release of OS X. All of the servers out there running Linux, *BSD, Solaris, and even Windows use other languages (such as PHP, Perl, Ruby, and the shell) to accomplish their necessary scripting tasks. But these languaes excel where AppleScript was never intended to go; the original Mac OS never had a command–line and typically needed a specialized ports of a language in order for it to be compatible (such as MacPerl). Indeed, AppleScript is a language used to accomplish what traditional scripting languages are unable to do: exchange information between applications on a higher level than is possible through plain–text pipes on the command–line; Perl has no idea how to automate Photoshop, and AppleScript has poor text–processing abilities and rather poor performance in terms of raw speed.

AppleScript, of course, also has its advantages; through it you can gain access to information that is inaccessible to the cross–platform scripting languages (or even high–level languages such as Java and C’s derivatives) mentioned above. For example, virtually every programming language under the sun can access the standard information regarding a file stored on your hard drive, such as:

However, the HFS(+) filesystem also uses resource forks to store metadata for files, which “traditional” languages can’t access without special methods. It is here, in the resource forks, where a file’s metadata are stored (such as its label & comment).

Figure #2: The Finder’s labels and comments are two examples of information stored in a file’s resource fork.

So… what does this have to do with Apache? Well, I’m about to get to that.

Playing With Apache

In my previous article titled “Mo’ Betta Indexes,” I outlined how to customize Apache’s index pages to match the look and feel of their surrounding site by changing virtually everything about the page’s appearance, including:

In doing so, I turned a stock Apache page into something a little more pleasing to the eye:

A customized index page automatically generated by Apache

Figure #3: A customized index page automatically generated by Apache.

One other part of the index that we altered was the “Description,” which allows a brief comment to be added to a file (or group of files) using the AddDescription directive:

AddDescription "description" filename

For example, the following excerpt of a configuration file was used to add the descriptions shown in the above screenshot (line breaks have been added for display purposes, but each directive should only be one line):

AddDescription "<strong>Please read me</strong>" *README
AddDescription "<a title=\"PHP: Hypertext Preprocessor\" 
	href=\"http://www.php.net\">PHP</a> Page" *.php
AddDescription	"<acronym
	title=\"HyperText Markup Language\">HTML</acronym>
	Page" *.html *.htm
AddDescription "<acronym
	title=\"Portable Network Graphics\">PNG</acronym>
	Image" 	*.png
AddDescription "<acronym
	title=\"Graphics Interchange Format\">GIF</acronym>
	Image" *.gif
AddDescription "<acronym
	title=\"Joint Photographic Experts Group\">JPEG</acronym>
	Image" *.jpg *.jpeg
AddDescription "Plain-text Document" *.txt
AddDescription "<acronym title=\"Motion Picture Experts Group
	Audio Layer 3\">MP3</acronym> Audio File" *.mp3
AddDescription "Zip Archive" *.zip
AddDescription "<a
	title=\"Quicktime\">QuickTime</a> Movie" *.mov

The Problem

The descriptions listed above serve as great defaults, but they are by no means the most detailed descriptions for files; for example, every HTML file (one that ends with “.htm” or “.html” according to the definition given above) will have the exact same description as every other HTML file.

This problem can be solved by adding detailed descriptions for files through more AddDescription directives. However, this brings about the problem of maintainability — every time a file is added to the web documents folder, another directive will need to be added to over–ride the default value. This won’t be a problem for a small static site, but sites that have hundreds of frequently changing files that will have problems keeping abreast of any changes. Thankfully, AppleScript and a little bit of UNIX knowledge can solve our problem.

The Solution

Since AppleScript gives scripters access to the metadata — including comments — for files, we are able to automatically generate all of the AddDescription directives without having to manually edit any configuration files. Instead, we’ll simply use the Finder’s “Get Info” window to enter our comments and let our script take care of the rest.

Configuration Notes

Before we get started, there is an important design factor that must be take into consideration; this is the fact that there are two possible places that we can place our AddDescription directives:

  1. place all of the directives within a central configuration file, which will then act as a repository of descriptions,
  2. or place the directives for the files in a folder within a plain–text access file contained within that folder, thereby breaking the information up into bite–sized chunks.

Each method has its pros and cons; Apache will read access files everytime a request is made for a file in the folder, which results in more disk activity than if the directives were placed in a configuration file that is only read when the daemon is started. However, configuration files can be modified while the daemon is running, which will allow us to generate the files and have them take effect immediately, whereas the daemon will have to be restarted in order for a change in a configuratin file to take effect.

I chose to take method #2. However, in the event that the first method suits your needs better, the script can be easily modified to make use of it (although this will be one of those exercises that the author leaves to the reader to attempt).

It should be noted that there is also one minor configuration change that might be required for the access files to take effect: by default, most Apache installations don’t permit access files to override the global configuration settings. Because our AddDescription directives will be placed in access files, we will have to instruct Apache to use them by replacing the command

AllowOverride None

in /private/etc/httpd/httpd.conf with the following:

AllowOverride Indexes

Or, if you are already using access files, simply add the Indexes keyword to the list of options to allow:

AllowOverride AuthConfig Indexes
Editing Apache’s configuration file in BBEdit

Figure #4: Editing Apache’s configuration file in BBEdit.

There is one more directive that will affect how our script operates, and it tells Apache what the name of the access file is. So if for some reason you need to use a different name, you can do it with the following directive:

AccessFileName filename

These files are given the name .htaccess so that they won’t show up in the Finder (or a directory listing in the terminal) or in Apache’s own directory listings by default. Thus, the directive in Apache’s standard configuration file is

AccessFileName .htaccess

Important: Always be careful when editing configuration files; it is never a bad idea to create a backup even for minor changes in the event that you make a catastrophic typo and are unable to restore it to a working state. More information on the AllowOverride and AccessFileName directives can be found in Apache’s manual. With that in mind, let’s move on to the script.

The Plan

So we now know what the script must do, but we have yet to design exactly how it will do its job. The following guideline outlines the script’s requirements while executing:

  1. initialize the .htaccess file in the selected folder,
  2. add an AddDescription directive for each file in the folder that has a comment,
  3. and repeat for each sub–folder of the selected folder, thereby drilling down into the document tree and visiting all sub–folders.

Pretty simple, no? Apart from that whole recursion thing, of course. And while the basic algorithm listed above glosses over the details of exactly how to implement each step, we’ll cover that in the rest of this article. If you prefer not to go through the code line–by–line, here is a zipped copy of the script so that you can get going right away. For those who are not well–versed in AppleScript and wish to learn a new trick or two or are just curious how it works, read on.

I should note that, for the sake of brevity, not all of the script will be described in detail; as a large portion of it is trivial and need not be dissected. Instead, only the more important aspects will be covered. However, the source code is thoroughly documented so even those new to AppleScript will be able to understand its operation.

Step #1: Initialize the Access File

In order to generate the comments for our website, the very first thing we’ll have to do is get a reference to the document root — we never move above this folder, only below. This folder could be /Library/WebServer/Documents, ~/Sites/, or if you have a virtual host enabled on your machine, pretty much any directory in the filesystem. With AppleScript, specifying a reference to a folder at run–time can be done in one of two ways:

  1. by dragging a folder (or a series of folders) onto the droplet from within the Finder,
  2. or making a call to the choose folder function from within your script.

To allow both methods to be used, we will save the script as an application and create a sub–routine that is invoked from each method handler, named appropriately enough, addComments:

(* invoked when files are dragged onto the script
within the Finder *)
on open (myfiles)
  tell application "Finder"
    repeat with i in myfiles
      my addComments(i)
    end repeat
  end tell
end open

(* invoked when the script is opened from the Finder
(either double-click, or via Script Menu *)
on run
  set myfile to choose folder
  my addComments(myfile)
end run
Dropping our web document folder onto the script’s droplet for processing

Figure #5: Dropping our web document folder onto the script’s droplet for processing.

The meat and potatoes of the script is contained within the addComments sub–routine, although it relies on several other sub–routines for assistance:

on addComments(myfile)
  set myPOSIX to (POSIX path of myfile as string)
  (* check to see if this is a folder*)
  if (my isFolder(myfile)) then
    tell application "Finder"
      (* make sure there's an access file in this directory *)
      set accessfile to my prepareAccessFile(myPOSIX)

      (* add a description for the parent folder *)
      set myparent to parent of myfile
      my addDescription(accessfile, "..", comment of myparent)

      (* add a description for every file/folder... *)
      set myfiles to items of myfile
      repeat with i in myfiles
        set m to i as alias
        (* add an entry for it in the access file *)
        my addDescription(accessfile, name of i, comment of i)

        (* and repeat this process with it if its a subfolder *)
        if (my isFolder(m)) then
          my addComments(m)
        end if
      end repeat

    end tell

  (* if it's not a folder, then ignore it*)
  end if
end addComments

The addComments routine is invoked with every file that is contained in the document tree of our website. And with each parameter that is passed to the routine, in addition to the file’s parent directory (denoted by “..”), an AddDescription directive is added to the Apache access file residing in the file’s directory. But before the description is added to the access file, we must ensure that there is an access file in place. Which turns out to be more complicated than it sounds.

Preparing the access file can take one of two courses:

This step is where the script becomes more complex than your run–of–the–mill AppleScript:

(* checks to see if a folder has a ACCESS_FILE in it;
   if yes, then formats it to start from scratch,
   if not then creates one *)
on prepareAccessFile(myPOSIX)

  (* get the path name that we will be testing *)	
  set myaccess to (myPOSIX as string) & ACCESS_FILENAME
  set accessposix to myaccess as POSIX file
  (* checks to see if the access file already exists *)
  if (my hasAccessFile(myPOSIX)) then
    set myScript to
      ("perl -pi -e \"s/^(.*AddDescription.*)//sg\" ") &
      (quoted form of POSIX path of accessposix as string)
    do shell script myScript
    (* if no access file exists, then make one *)
    my makeAccessFile(myPOSIX)
  end if
  return accessposix
end prepareAccessFile

The first step is to determine whether or not an access file exists, which is accomplished via the hasAccessFile routine. This routine returns a boolean indicating whether or not an access file already exists in the directory:

(* returns true if the given folder has an ACCESS_FILE in it *)
on hasAccessFile(posixFolder)
  set myListing to do shell script "ls -a " &
    quoted form of posixFolder
  return (myListing contains ACCESS_FILENAME)
end hasAccessFile

The traditional way of doing this would be to tell the Finder to get a list of all the items in the folder and see if that list contains the access file. However, because our access files are hidden by the Finder (that is, they begin with a “.”), the Finder won’t include it in the list of files. This can be remedied by using one of the various hacks (such as this one) to instruct the Finder to list all files in a directory. But to avoid such trickery, we’ll use the shell to leave the Finder’s preferences intact. ls, of course, is the UNIX command to list all files in a directory, and the “-a” flag will tell the command to include all files in the listing, even hidden ones.

If the access file exists, then all AddDescription directives contained within it must be deleted, as they may contain descriptions that are out of date. There are several ways of accomplishing this step, each with its own pros and cons:

After all of the discussion earlier in the article regarding AppleScript and UNIX, it should come as no surprise that the third method was chosen: it is the only way to accomplish the task at hand without requiring some kind of extension, whether it be a scripting addition or application.

The following statements will do exactly what we need:

 set myScript to
  ("perl -pi -e \"s/^(.*AddDescription.*)//sg\" ") &
  (quoted form of POSIX path of accessposix as string)
 do shell script myScript

For those in the audience that don’t happen to be Perl hackers — I happen to fall into this category myself — the above command will use the Perl interpreter to scan through the access file and remove all lines that contain the string “AddDescription” (as shown in Geoffrey Bradwell’s article, “FMTYEWTK About Mass Edits In Perl”). So we now have a fresh access file devoid of AddDescription directives, assuming it existed before running our script.

But what happens if it doesn’t exist? Well, we’re going to have go ahead and create it; and that’s what the makeAccessFile routine takes care of:

(* creates an ACCESS_FILE in the given directory *)
on makeAccessFile(posixPath)
  (* build the path where the file should be located *)
  set myquoted to quoted form of posixPath
  set myquoted to (characters 1 thru
    ((length of myquoted) - 1) of myquoted)
  set mypath to
    (myquoted & ACCESS_FILENAME & "'") as string
  (* create the file *)
  do shell script "touch " & mypath &
    "; chmod 744 " & mypath
  (* ensure the proper permissions are set *)
  do shell script "chmod 744 " & mypath
  (* and add a comment indicating when it was generated*)
  set myScript to "echo \"" & ACCESS_COMMENT &
    "\" >> " & mypath
  do shell script myScript
end makeAccessFile

This routine takes a similar route to creating the access file: instead of telling the Finder to create the file, the UNIX command touch is used, and then the proper permissions are set on the file. The last call to do shell script merely adds a comment to the top of the file to mark when and how the file was created.

So there are only two things left to do: add the actual AddDescription directives to the access file for each file, and then recursively invoke addComments with any sub–folders of the current folder. And thankfully, both steps are much simpler than the code we just walked through.

Steps #2 and #3: Create the AddDescription Directive and Recurse

The next block of code in the addComments routine is the following:

repeat with i in myfiles
  set m to i as alias
  my addDescription(accessfile, name of i, comment of i)
  if (my isFolder(m)) then
    my addComments(m)
  end if
end repeat

The above block accomplishes steps #1 and #2 of our algorithm — add the comments for the current file and then dig down into it if it’s a folder. The addDescription routine is shown below:

(* adds an AddDescription directive into the
   specified access file *)
on addDescription(accessposix, fileName, comments)
  set mypath to POSIX path of accessposix
  set myquote to quoted form of mypath

  if (comments is not equal to "") then
    set myScript to "echo \"AddDescription \\\"" &
    my urlencode(comments) & "\\\" \\\"" & my escape(fileName) &
    "\\\" \" >> " & (myquote)
    set a to do shell script (myScript)
  end if
end addDescription

Again, we are using a UNIX command to add the directive to the end of the access file; the name of the file is escaped (in case it contains a quote, backslash, or another mis–behaving character) and the comment is URL–encoded.

Several sections of the script weren’t covered in this article (such as the urlencode and urlescape routines for embedding the filname and its comment within the UNIX command); rest assured that they aren’t directly involved in the script’s functionality.

Putting the Script to Use

So we’ve walked through the code for the script, now let’s see it in action. The following files all reside in the same folder within my web document root (/Library/WebServer/Documents), and each have a comment applied to them through the Finder’s “Get Info” window:

Setting comments for the files in a folder of our web document folder

Figure #6: Setting comments for the files in a folder of our web document folder.

And the following is the index page generated by Apache, which uses the information covered in my previous article “Mo’ Betta Indexes” and the access files generated by the script:

The result of all our hard work

Figure #7: The result of all our hard work. Click for a larger version.

Although a lot of effort has been expended in to create this one index page, we can now leverage our toolset to create as many pages as we want and have most of the content generated for us, not by us. All we have to do is click a couple buttons here and there.

Wrapping Up

In this article we put AppleScript to use in a way that it was not originally designed to be used. While many developers trained in high–level languages such as Java and C despise AppleScript because of its verbose and unstructured syntax, it still plays an integral role within OS X. And with the release of Automator in the next version of OS X, scripters are going to be able to build even more complex tools to suit their needs.