Nathan Grigg

Brute force

One of the main lessons that I have learned in the last three years in my job as a programmer is that if you only have to do a job once or twice, use brute force.

Today, I was setting up a Synology NAS. The instructions for getting started are extremely sparse, consisting mainly of “type in the IP address in a web browser and follow the instructions there.”

In the old days, I would have started typing IP addresses until I found one that worked. Today, I made a new directory called “ips” and ran

for i in $(seq 50); do
  echo "curl 10.0.1.$i > $i"
done | parallel -j 25

Then, since I am (obviously) impatient, I ran wc -l * every few seconds to see which files were not empty. I found it at

Later I discovered, which is the official way to find your Synology.

I got an Apple Watch for my birthday. So far, the best part is easy access to my calendar while I’m at work.

I would like to write more regularly, so I’m experimenting with a shorter post format, what Manton Reece calls a microblog post. I spent some time today tweaking my Jekyll configuration to enable them on this site.

Uploading an Image with Workflow and Flask

Workflow is an iOS app that lets you build a simple program by dragging blocks around, similar to Apple’s Automator app that ships with macOS. A recent update makes it possible to send a wider variety of HTTP requests, which allows you to interact with web APIs that aren’t otherwise supported.

Or, if you have a web server, write your own API.

Upload images workflow

Here is a workflow to take images from my phone and upload them to my server. It makes one request per image. It sets the custom header Grigg-Authentication to make sure that random people aren’t uploading images. It puts a file into the POST request with field name image. The responses will be HTML image tags, which are collected and then copied to the clipboard.


Flask is a Python web framework. It makes it very easy to map URLs to Python functions.

The first thing I wrote was a private decorator, that would check the HTTP headers for my authentication key. It doesn’t have to be a decorator, but that makes it easier to reuse in the future.

1 KEY = 'password1!'
3 def private(f):
4     @functools.wraps(f)
5     def wrapper(*args, **kwargs):
6         if flask.request.headers.get('Grigg-Authenticate') != KEY:
7             flask.abort(403)
8         return f(*args, **kwargs)
9     return wrapper

If you are not using a secure (HTTPS) connection, somebody could read your authentication key and pretend to be you. You can set this up directly with Flask, but since I’m already running nginx, I used that. (I will share the details in a future post.)

Next, there is some basic Flask setup. I changed the response MIME type to plain text and registered an error handler that will report any exceptions in the response, rather than logging an error where I won’t see it.

 1 app = flask.Flask(__name__)
 3 class TextResponse(flask.Response):
 4     default_mimetype = 'text/plain'
 6 app.response_class = TextResponse
 8 @app.errorhandler(Exception)
 9 def handle_generic_exception(e):
10     return 'Unhandled exception: {!r}\n'.format(e)

Then, there is the routing code. This function is called every time someone visits /blog/upload-image, as specified in the route decorator.

 1 @app.route('/blog/upload-image', methods=['POST'])
 2 @private
 3 def blog_upload_image():
 4     try:
 5         fh = flask.request.files['image']
 6     except KeyError:
 7         flask.abort(400, 'Expected a file with key "image", not found')
 9     _, extension = os.path.splitext(fh.filename)
10     filename = upload_image(fh, extension)
11     return '<img src="{}" class="centered">\n'.format(filename)

Finally, the actual work is done by the upload_image function. I save the image into a dated directory with a random filename, then run a bunch of git commands.

 1 class Error(Exception):
 2     pass
 4 def random_chars(size):
 5     return base64.b32encode(
 6             uuid.uuid4().bytes).decode('ascii').lower().rstrip('=')[:size]
 8 def upload_image(fh, extension):
 9     """Upload image to blog and return filename, relative to site root."""
10     subdir = 'images/{:%Y}'.format(
11     try:
12         os.mkdir(os.path.join(PATH, subdir))
13     except FileExistsError:
14         pass
16     basename = ''.join((random_chars(8), extension))
17     filename = os.path.join(subdir, basename)
18, filename))
20     output = []
21     def run(args):
22         output.append(' '.join(args))
23         output.append(subprocess.check_output(
24             args, cwd=PATH, stderr=subprocess.STDOUT))
25     try:
26         run(['git', 'pull', '--ff-only'])
27         run(['git', 'add', filename])
28         run(['git', 'commit', '-m', 'Add image'])
29         run(['git', 'push'])
30     except subprocess.CalledProcessError:
31         raise Error('Git operation failed. Output:\n{}'.format(
32             '\n'.join(output)))
34     return filename

Time Zone News

If you like time zones—who doesn’t?—you should check out Time Zone News. Once a month or so, I get gems like this in my news feed:

Haiti cancels daylight saving time with two days notice

The planned change to daylight saving time in Haiti at 2 am local time on 13 March 2016 has been cancelled.

Or this one:

Chile reintroduces DST

Chile’s Ministry of Energy announced today that Chile will be observing daylight saving time again. Chile Standard Time will be changed back to UTC -4 at 00:00 on 15 May, and DST will be observed from 00:00 on 14 August 2016, changing time in Chile to UTC -3.

Chile used to observe DST every year until a permanent UTC offset of -3 was introduced in 2015.

It is unclear whether the time change also applies to Easter Island.

Filter RSS

I was looking to make more room on my phone’s home screen, and I realized that my use of had dwindled more than enough to remove it. I never post any more, but there are a couple of people I would still like to follow that don’t cross post to Twitter. has RSS feeds for every user, but they include both posts and replies. I only want to see posts. So I brushed off my primitive XSLT skills.

I wrote an XSLT program to delete RSS items that begin with @. While I was at it, I replaced each title with the user’s name, since the text of the post is also available in the description tag.

Here is the transformation that would filter my posts, if I had any:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <xsl:stylesheet version="1.0"
 3     xmlns:xsl="">
 5 <!-- Default identity transformation -->
 6 <xsl:template match="@*|node()">
 7     <xsl:copy>
 8         <xsl:apply-templates select="@*|node()"/>
 9     </xsl:copy>
10 </xsl:template>
12 <!-- Replace title with my username -->
13 <xsl:template match="item/title/text()">nathangrigg</xsl:template>
15 <!-- Remove completely items which are directed at other users.
16      The RSS feed has titles of the form @username: text of post. -->
17 <xsl:template match="item[contains(title, '@nathangrigg: @')]" />
18 </xsl:stylesheet>

Now I can use xsltproc to filter the RSS. In order to fill in the username automatically, I wrapped the XSLT program in a shell script that also invokes curl.

 1 #!/bin/bash
 2 set -o errexit
 3 set -o pipefail
 4 set -o nounset
 6 if (( $# != 1 )); then
 7     >&2 echo "USAGE: $0 username"
 8     exit 1
 9 fi
11 username=$1
13 xslt() {
14 cat << EOM
15 <?xml version="1.0" encoding="UTF-8"?>
16 <xsl:stylesheet version="1.0"
17     xmlns:xsl="">
19 <!-- Default identity transformation -->
20 <xsl:template match="@*|node()">
21     <xsl:copy>
22         <xsl:apply-templates select="@*|node()"/>
23     </xsl:copy>
24 </xsl:template>
25 <!-- Replace title with just the username -->
26 <xsl:template match="item/title/text()">$username</xsl:template>
27 <!-- Remove completely items which are directed at other users.
28         The RSS feed has titles of the form @username: text of post. -->
29 <xsl:template match="item[contains(title, '@$username: @')]" />
30 </xsl:stylesheet>
31 EOM
32 }
34 rss() {
35     curl --silent --fail$username/posts
36 }
38 xsltproc <(xslt) <(rss)