From 45a913ee917faff9b40e7ee2dfbcd1fc375fcb9d Mon Sep 17 00:00:00 2001
From: Abdullahi Yunus <abdoollahikbk@gmail.com>
Date: Wed, 5 Feb 2025 14:36:10 +0100
Subject: [PATCH] lnutils: add createdir util function

This utility function replaces repetitive logic patterns
throughout LND.
---
 lnutils/fs.go      | 31 +++++++++++++++++
 lnutils/fs_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 118 insertions(+)
 create mode 100644 lnutils/fs.go
 create mode 100644 lnutils/fs_test.go

diff --git a/lnutils/fs.go b/lnutils/fs.go
new file mode 100644
index 000000000..bc09176cc
--- /dev/null
+++ b/lnutils/fs.go
@@ -0,0 +1,31 @@
+package lnutils
+
+import (
+	"errors"
+	"fmt"
+	"os"
+)
+
+// CreateDir creates a directory if it doesn't exist and also handles
+// symlink-related errors with user-friendly messages. It creates all necessary
+// parent directories with the specified permissions.
+func CreateDir(dir string, perm os.FileMode) error {
+	err := os.MkdirAll(dir, perm)
+	if err == nil {
+		return nil
+	}
+
+	// Show a nicer error message if it's because a symlink
+	// is linked to a directory that does not exist
+	// (probably because it's not mounted).
+	var pathErr *os.PathError
+	if errors.As(err, &pathErr) && os.IsExist(err) {
+		link, lerr := os.Readlink(pathErr.Path)
+		if lerr == nil {
+			return fmt.Errorf("is symlink %s -> %s "+
+				"mounted?", pathErr.Path, link)
+		}
+	}
+
+	return fmt.Errorf("failed to create directory '%s': %w", dir, err)
+}
diff --git a/lnutils/fs_test.go b/lnutils/fs_test.go
new file mode 100644
index 000000000..3e96d4faf
--- /dev/null
+++ b/lnutils/fs_test.go
@@ -0,0 +1,87 @@
+package lnutils
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+// TestCreateDir verifies the behavior of CreateDir function in various
+// scenarios:
+// - Creating a new directory when it doesn't exist
+// - Handling an already existing directory
+// - Dealing with symlinks pointing to non-existent directories
+// - Handling invalid paths
+// The test uses a temporary directory and runs multiple test cases to ensure
+// proper directory creation, permission settings, and error handling.
+func TestCreateDir(t *testing.T) {
+	t.Parallel()
+
+	tempDir := t.TempDir()
+
+	tests := []struct {
+		name      string
+		setup     func() string
+		wantError bool
+	}{
+		{
+			name: "create directory",
+			setup: func() string {
+				return filepath.Join(tempDir, "testdir")
+			},
+			wantError: false,
+		},
+		{
+			name: "existing directory",
+			setup: func() string {
+				dir := filepath.Join(tempDir, "testdir")
+				err := os.Mkdir(dir, 0700)
+				require.NoError(t, err)
+
+				return dir
+			},
+			wantError: false,
+		},
+		{
+			name: "symlink to non-existent directory",
+			setup: func() string {
+				dir := filepath.Join(tempDir, "testdir")
+				symlink := filepath.Join(tempDir, "symlink")
+				err := os.Symlink(dir, symlink)
+				require.NoError(t, err)
+
+				return symlink
+			},
+			wantError: true,
+		},
+		{
+			name: "invalid path",
+			setup: func() string {
+				return string([]byte{0})
+			},
+			wantError: true,
+		},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			dir := tc.setup()
+			defer os.RemoveAll(dir)
+
+			err := CreateDir(dir, 0700)
+			if tc.wantError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			info, err := os.Stat(dir)
+			require.NoError(t, err)
+			require.True(t, info.IsDir())
+		})
+	}
+}