UP | HOME

CL Script Maildir Backup

Introduction

Inspired by the book “Automate the boring stuff with Python” I constantly automate boring stuff on my Linux machine. But using Common Lisp instead of Python. I think learning a programming language is like learning everything else in life. Apply theoretical knowledge (programming language) to something helpful or useful and you will be more motivated to learn it and it will be harder to forget about it. Programming is all about solving computer problems. If you do not have a computer problem, what is the point of learning a programming language?

What is a computer problem?

A computer problem is the following: I am using a program called mbsync (or isync) to have my emails available on my computer even without internet access.

isync is a command line application which synchronizes mailboxes;

isync solves that problem very well. Here is another problem I do have: I want to periodically backup the ~/Maildir folder (the folder my emails are synced), versioned by date and zipped on my computer to my Nextcloud instance. The reason for this is: What if my Maildir folder got corrupted? Or my email provider experienced a service outage?

Let us break down the problem into smaller steps:

  1. Call isync to synchronize emails.
  2. Create a zip file of ~/Maildir folder.
  3. Move this zip file to a sync folder of Nextcloud.
  4. Remove older backups.
  5. Do this periodically.

The bash script phenomenon

In the world of Linux people often tend to solve their computer problems using bash scripting. And a bash script which solves my computer problem backing up my Maildir folder to Nextcloud could be like the following:

#!/bin/bash
# -*- mode: shell-script; coding: utf-8; -*-

# Set variables
MAILDIR="$HOME/Maildir"
BACKUP_DIR="$HOME/Documents"
DATE=$(date +%Y%m%d)
BACKUP_FILE="maildir_backup_$DATE.zip"

# Run mbsync
mbsync -a

# Create zip of Maildir
zip -r "/tmp/$BACKUP_FILE" "$MAILDIR"

# Move zip file to Documents folder
mv "/tmp/$BACKUP_FILE" "$BACKUP_DIR"

# Optional: Remove old backups (keeping last 5)
cd "$BACKUP_DIR" && ls -t maildir_backup_*.zip | tail -n +6 | xargs -r rm

A nice and easy bash script. But often computer problems tend to grow and scripts has to grow too. And when source code is growing it is getting difficult to maintain. This script is using global variables which can be overwritten easily by other scripts or name conflicts can happen at the time of execution. DATE seems like a very general name for a variable. Maybe it is already defined in my .bashrc file or otherwise loaded into my bash environment?

Some important things

Before we beginn to rewrite the solution to my computer problem using Common Lisp I need to introduce you to some important things in Common Lisp:

  • UIOP Library
  • Pathname Objects

UIOP Library

the Utilities for Implementation- and OS- Portability.

UIOP is the portability layer of ASDF. It provides utilities that abstract over discrepancies between implementations, between operating systems, and between what the standard provides and what programmers actually need, to write portable Common Lisp programs.

UIOP simplifies to write implementation independent Common Lisp code for the programmer. So the first line of code needed in our Common Lisp file is:

(require :uiop) ; Load UIOP Library

We will make use of uiop:run-program to call mbsync and zip programs.

Pathname objects

There are no such thing as files in Common Lisp. Instead Common Lisp implementations are using a framework in which files are named by a Lisp data object of type pathname.1

Here are some important pathname functions to remember:

user-homedir-pathname
Returns the user home directory as pathname.
merge-pathnames
Merges two pathnames into one.
(merge-pathnames #P"Maildir/" (user-homedir-pathname))
#P"/home/marcus/Maildir/"

Using pathname objects in Common Lisp instead of strings for working with files offers several benefits, aligning well with functional programming principles. Here are the key advantages:

  1. Abstraction and platform independence: Pathname objects provide a higher-level abstraction for file paths, making your code more portable across different operating systems. The system automatically handles differences in path separators and conventions.
  2. Component-based manipulation: Pathnames allow you to work with individual components (device, directory, name, type) separately, which is more functional and less error-prone than string manipulation.
  3. Type safety: Using pathnames provides better type safety, as the system can check for valid pathname objects rather than arbitrary strings.
  4. Built-in operations: Common Lisp provides many built-in functions for working with pathnames, making file operations more convenient and less error-prone.

Rewrite the bash script

Get today as date

The first smaller problem we want to address is getting a versioned pathname as maildir_backup_20240911.zip. To achieve this we need to get the date at the time the script is executed with a function called get-date. Common Lisp implementations provides us with two important functions related to date and time:

get-universal-time
Counts seconds since January 1, 1900, at midnight UTC.
decode-universal-time
Parses get-universal-time and returns multiple values related to date and time.
(multiple-value-bind (second minute hour date month year)
    (decode-universal-time (get-universal-time) 0)
  (format nil "~4,'0d~2,'0d~2,'0d" year month date))
20240923

By using the function get-date we are able to construct a versioned file name.

(defun get-date ()
  (multiple-value-bind (second minute hour date month year)
      (decode-universal-time (get-universal-time))
    (format nil "~4,'0d~2,'0d~2,'0d" year month date)))

Footnotes:

Author: Marcus Kammer

Email: marcus.kammer@mailbox.org

Date: Mon, 30 Sep 2024 09:46 +0200

Emacs 29.1.90 (Org mode 9.6.11)

License: CC BY 4.0