Setting up a writable file system on top of a DwarFS image

Apr 12, 2024

This shows how to set up a read/write layer on top of a read-only DwarFS image, incredibly handy if you need to modify or extend the contents of your read-only image temporarily.

My primary use case is storing highly compressed NATS dumps in a DwarFS image. Then, I set up a read/write layer to potentially alter these dumps as needed. Once the changes are no longer needed, I can simply wipe the read/write layer to return to the original NATS dumps.

Create a DwarFS image

Create a DwarFS image with the NATS dumps. This is a one-time operation.

mkdwfs -i nats -o nats-2023.dwarfs

Prepare directories

Create a set of directories for the original data overlay.

mkdir nats-2023 && cd nats-2023

mkdir nats-ro
mkdir nats-rw
mkdir nats-work
mkdir nats

Mount the DwarFS image

Mount the DwarFS image. -o allow_root is necessary to ensure overlayfs has access to the mounted file system. You might need to adjust /etc/fuse.conf by uncommenting or adding user_allow_other.

dwarfs ../nats-2023.dwarfs nats-ro -o allow_root

Now set up overlayfs:

sudo mount -t overlay overlay -o lowerdir=nats-ro,upperdir=nats-rw,workdir=nats-work nats

You should now have access to a writable version of your DwarFS image at nats-2023/nats.

Unmount the overlayfs

When you're done with the changes, unmount the overlayfs:

sudo umount nats

Keep the changes

You can go further than this. Suppose you have different sets of modifications you regularly want to apply to the base DwarFS image. In that case, you can build a new DwarFS image from the read-write directory after unmounting the overlayfs and selectively add this by passing a colon-separated list to the lowerdir option when setting up the overlayfs mount:

sudo mount -t overlay overlay -o lowerdir=nats-ro:additional-modules nats

If you want this merged overlay to be writable, just add in the upperdir and workdir options from before again.

Script it

Here's a script that simplifies the process.

#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset

# Default directory setup
display_full_usage() {
    cat <<EOF
NAME
       $(basename "$0") - Manages DwarFS images.

COMMANDS
       create <source-dir> [output-path] [-c, --check-perms]
            Creates a compressed read-only DwarFS image from 'source-dir' dir.
            Optionally checks for files without group write permissions.

       mount <mount-point> <image-path>
            Mounts the 'image-path' image to 'mount-point' and sets up a writable overlay.

       umount <mount-point>
            Unmounts the DwarFS and overlay filesystems at 'mount-point'.

OPTIONS
       -h, --help
           Displays this help message and exits.

       -c, --check-perms
           Checks if the source directory has files without 'g+w' permissions.

EOF
}

command=""
args=()
check_perms=0

parse_args() {
    local stop_processing_options=0

    # Parse command-line arguments
    while [[ $# -gt 0 ]]; do
        if [[ $stop_processing_options -eq 1 ]]; then
            args+=("$1")
            shift
            continue
        fi

        case $1 in
        -h | --help)
            display_full_usage
            exit 0
            ;;
        -c | --check-perms)
            check_perms=1
            shift
            ;;
        --)
            stop_processing_options=1
            shift
            ;;
        -*)
            echo "Unknown option: $1"
            exit 1
            ;;
        *)
            if [[ -z "$command" ]]; then
                command="$1"
            else
                args+=("$1")
            fi
            shift
            ;;
        esac
    done

    if [[ -z "${command:-}" ]]; then
        echo "error: no command specified"
        display_full_usage
        exit 1
    fi
}

check_dependencies() {
    if ! command -v mkdwarfs &>/dev/null; then
        echo "error: mkdwarfs is not installed. Install dwarfs: https://github.com/mhx/dwarfs"
        exit 1
    fi

    if ! command -v dwarfs &>/dev/null; then
        echo "error: dwarfs is not installed. Install dwarfs: https://github.com/mhx/dwarfs"
        exit 1
    fi
}

create_image() {
    local source_dir="$1"
    local default_output_path
    default_output_path="$(basename "${source_dir}".dwarfs)"
    local output_path="${2:-$default_output_path}"

    # check if output path ends with .dwarfs else append it
    if [[ ! "$output_path" =~ \.dwarfs$ ]]; then
        output_path="${output_path}.dwarfs"
    fi

    if [[ $check_perms -eq 1 ]]; then
        if ! check_file_permissions "$source_dir"; then
            echo "Permission check failed. Some files don't have 'g+w' permissions."
            echo "Run the following command to fix permissions:"
            echo "chmod -R g+w $source_dir"
            exit 1
        fi
    fi

    echo "Creating DwarFS image from '${source_dir}' directory..."
    mkdwarfs -i "$source_dir" -o "${output_path}"
    echo "DwarFS image created at ${output_path}"
}

check_file_permissions() {
    local dir="$1"
    if find "$dir" -type f ! -perm /g+w -print -quit | grep -q '.'; then
        return 1
    fi

    return 0
}

mount_image() {
    local mount_point="$1"
    local image_path="$2"

    echo "Mounting DwarFS image..."
    local read_only_dir="${mount_point}-ro"
    local read_write_dir="${mount_point}-rw"
    local work_dir="${mount_point}-work"
    mkdir -p "$read_only_dir" "$read_write_dir" "$work_dir" "$mount_point"
    dwarfs "$image_path" "$read_only_dir" -o allow_root
    sudo mount -t overlay overlay -o lowerdir="$read_only_dir",upperdir="$read_write_dir",workdir="$work_dir" "$mount_point"
    echo "DwarFS image mounted at $mount_point"
}

unmount_image() {
    local mount_point="$1"
    echo "Unmounting overlay and DwarFS image at '${mount_point}'..."
    sudo umount "$mount_point"
    sudo umount "${mount_point}-ro"
    echo "Unmounted DwarFS image at '${mount_point}'"
}

execute_command() {
    case $command in
    create)
        if [[ ${#args[@]} -lt 1 ]]; then
            echo "err: create command requires one argument for source directory."
            display_full_usage
            exit 1
        fi
        create_image "${args[@]}"
        ;;
    mount)
        if [[ ${#args[@]} -ne 2 ]]; then
            echo "err: mount command requires exactly two arguments: mount point and image path."
            display_full_usage
            exit 1
        fi
        mount_image "${args[0]}" "${args[1]}"
        ;;
    unmount | umount)
        if [[ ${#args[@]} -ne 1 ]]; then
            echo "err: unmount command requires exactly one argument for mount point."
            display_full_usage
            exit 1
        fi
        unmount_image "${args[0]}"
        ;;
    *)
        echo "Invalid command: $command"
        display_full_usage
        exit 1
        ;;
    esac
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    parse_args "$@"
    check_dependencies
    execute_command
fi

I have it saved as compressfs and use it like this:

 compressfs -h
NAME
       compressfs - Manages DwarFS images.

COMMANDS
       create <source-dir> [output-path]
            Creates a compressed read-only DwarFS image from 'source-dir' dir.

       mount <image-path> <mount-point>
            Mounts the 'image-path' image to 'mount-point' and sets up a writable overlay.

       umount <mount-point>
            Unmounts the DwarFS and overlay filesystems at 'mount-point'.

OPTIONS
       -h, --help
           Displays this help message and exits.

Create a DwarFS image:

compressfs create nats-2023

Mount the DwarFS image:

compressfs mount mounts/nats-2023 nats-2023.dwarfs

Unmount the DwarFS image:

compressfs umount mounts/nats-2023