Symlink redirects in nginx

Solved an interesting problem this week using nginx.

We have an internal nginx webserver for distributing datasets with dated filenames, like foobar-20190213.tar.gz. We also create a symlink called foobar-latest.tar.gz, that is updated to point to the latest dataset each time a new version is released. This allows users to just use a fixed url to grab the latest release, rather than having to scrape the page to figure out which version is the latest.

Which generally works well. However, one wrinkle is that when you download via the symlink you end up with a file named with the symlink filename (foobar-latest.tar.gz), rather than a dated one. For some use cases this is fine, but for others you actually want to know what version of the dataset you are using.

What would be ideal would be a way to tell nginx to handle symlinks differently from other files. Specifically, if the requested file is a symlink, look up the file the symlink points to and issue a redirect to request that file. So you'd request foobar-latest.tar.gz, but you'd then be redirected to foobar-20190213.tar.gz instead. This gets you the best of both worlds - a fixed url to request, but a dated filename delivered. (If you don't need dated filenames, of course, you just save to a fixed name of your choice.)

Nginx doesn't support this functionality directly, but it turns out it's pretty easy to configure - at least as long as your symlinks are strictly local (i.e. your target and your symlink both live in the same directory), and as long as you have the nginx embedded perl module included in your nginx install (the one from RHEL/CentOS EPEL does, for instance.)

Here's how:

1. Add a couple of helper directives in the http context (that's outside/as a sibling to your server section):

# Setup a variable with the dirname of the current uri
# cf.
map $uri $uri_dirname {
  ~^(?<capture>.*)/ $capture;

# Use the embedded perl module to return (relative) symlink target filenames
# cf.
perl_set $symlink_target_rel '
  sub {
    my $r = shift;
    my $filename = $r->filename;
    return "" if ! -l $filename;
    my $target = readlink($filename);
    $target =~ s!^.*/!!;    # strip path (if any)
    return $target;

2. In a location section (or similar), just add a test on $symlink_target_rel and issue a redirect using the variables we defined previously:

location / {
  autoindex on;

  # Symlink redirects FTW!
  if ($symlink_target_rel != "") {
    # Note this assumes that your symlink and target are in the same directory
    return 301$uri_dirname/$symlink_target_rel;

Now when you make a request to a symlinked resource you get redirected instead to the target, but everything else is handled using the standard nginx pathways.

$ curl -i -X HEAD
HTTP/1.1 301 Moved Permanently
Server: nginx/1.12.2
Date: Wed, 13 Feb 2019 05:23:11 GMT