Repository: creativeprojects/resticprofile (language: python)

Issue:
Load config from ~/.config/resticprofile on MacOS too
While I know that ~/.config is _technically_ not as standardized on MacOS as it is on Linux via the XDG-basedir specs, it is still a de facto standard place to put configuration files. It would be neat if resticprofile would start loading the profiles from ~/.config/resticprofile on MacOS the same way it does on linux just for the sake of consistency, and for the sake of potentially helping people that re-use some scripting or configuration across several systems, that version-control their .config (not that it necessarily makes sense to share a resticprofile config across computers, but there might be other tools that look at the config or interface with it in some way that benefit from it being in a standard location).


Discussion / hints:
I was just checking on my mac, and as it turns out I do have a few configs in the `~/.config` folder indeed. Although a lot less than the standard `~/Library/Application Support`.

I suppose it doesn't hurt to add it to the list 😉 


Fix patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 5f567ff7..87a24d73 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -197,7 +197,7 @@ brews:
     commit_author:
       name: goreleaser
       email: fred@creativeprojects.tech
-    folder: Formula
+    directory: Formula
     homepage: https://github.com/creativeprojects/{{ .ProjectName }}
     description: Configuration profiles for restic backup
     license: "GPL-3.0-only"
diff --git a/codecov.yml b/codecov.yml
index 53831104..f2ce922b 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -25,7 +25,7 @@ coverage:
       default:
         target: auto
         threshold: "2%"
-    patch: 
+    patch:
       default:
         target: "70%"
         threshold: "2%"
diff --git a/filesearch/filesearch.go b/filesearch/filesearch.go
index 0d55a634..ce9d9556 100644
--- a/filesearch/filesearch.go
+++ b/filesearch/filesearch.go
@@ -17,7 +17,7 @@ import (
 )
 
 var (
-	XDGAppName = "resticprofile"
+	AppName = "resticprofile"
 
 	// configurationExtensions list the possible extensions for the config file
 	configurationExtensions = []string{
@@ -40,6 +40,10 @@ var (
 		"/opt/local/etc/resticprofile/",
 	}
 
+	addConfigurationLocationsDarwin = []string{
+		".config/" + AppName + "/",
+	}
+
 	defaultConfigurationLocationsWindows = []string{
 		"c:\\restic\\",
 		"c:\\resticprofile\\",
@@ -65,7 +69,7 @@ var (
 	}
 )
 
-var fs afero.Fs
+var fs afero.Fs // we could probably change the implementation to use fs.FS instead
 
 func init() {
 	fs = afero.NewOsFs()
@@ -187,6 +191,8 @@ func FindResticBinary(configLocation string) (string, error) {
 			return filename, nil
 		}
 	}
+	clog.Tracef("could not find restic binary %q in any of these locations: %s", binaryFile, strings.Join(paths, ", "))
+
 	// Last resort, search from the OS PATH
 	filename, err := exec.LookPath(binaryFile)
 	if err != nil {
@@ -211,9 +217,11 @@ func ShellExpand(filename string) (string, error) {
 }
 
 func getSearchConfigurationLocations() []string {
-	locations := []string{filepath.Join(xdg.ConfigHome, XDGAppName)}
+	home, _ := os.UserHomeDir()
+
+	locations := []string{filepath.Join(xdg.ConfigHome, AppName)}
 	for _, configDir := range xdg.ConfigDirs {
-		locations = append(locations, filepath.Join(configDir, XDGAppName))
+		locations = append(locations, filepath.Join(configDir, AppName))
 	}
 
 	if platform.IsWindows() {
@@ -222,7 +230,11 @@ func getSearchConfigurationLocations() []string {
 		locations = append(locations, defaultConfigurationLocationsUnix...)
 	}
 
-	if home, err := os.UserHomeDir(); err == nil {
+	if platform.IsDarwin() {
+		locations = append(locations, addRootToRelativePaths(home, addConfigurationLocationsDarwin)...)
+	}
+
+	if home != "" {
 		locations = append(locations, home)
 	}
 
@@ -244,7 +256,7 @@ func getSearchBinaryLocations() []string {
 		paths = defaultBinaryLocationsUnix
 	}
 	if home, err := os.UserHomeDir(); err == nil {
-		paths = append(paths, filepath.Join(home, ".local/bin/"), filepath.Join(home, "bin/"))
+		paths = append(paths, addRootToRelativePaths(home, []string{".local/bin/", "bin/"})...)
 	}
 	return paths
 }
@@ -260,3 +272,22 @@ func fileExists(filename string) bool {
 	_, err := fs.Stat(filename)
 	return err == nil || errors.Is(err, iofs.ErrExist)
 }
+
+func addRootToRelativePaths(home string, paths []string) []string {
+	if platform.IsWindows() {
+		return paths
+	}
+	if home == "" {
+		return paths
+	}
+	rootedPaths := make([]string, len(paths))
+	for i, path := range paths {
+		if filepath.IsAbs(path) {
+			rootedPaths[i] = path
+			continue
+		}
+		path = strings.TrimPrefix(path, "~/")
+		rootedPaths[i] = filepath.Join(home, path)
+	}
+	return rootedPaths
+}
diff --git a/schedule/scheduler_config.go b/schedule/scheduler_config.go
index 0b8a73c6..8521dba5 100644
--- a/schedule/scheduler_config.go
+++ b/schedule/scheduler_config.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/creativeprojects/resticprofile/config"
 	"github.com/creativeprojects/resticprofile/constants"
-	"github.com/spf13/afero"
 )
 
 type SchedulerConfig interface {
@@ -35,16 +34,13 @@ type SchedulerWindows struct{}
 func (s SchedulerWindows) Type() string                     { return constants.SchedulerWindows }
 func (s SchedulerWindows) Convert(_ string) SchedulerConfig { return s }
 
-type SchedulerLaunchd struct {
-	Fs afero.Fs
-}
+type SchedulerLaunchd struct{}
 
 func (s SchedulerLaunchd) Type() string                     { return constants.SchedulerLaunchd }
 func (s SchedulerLaunchd) Convert(_ string) SchedulerConfig { return s }
 
 // SchedulerCrond configures crond compatible schedulers, either needs CrontabBinary or CrontabFile
 type SchedulerCrond struct {
-	Fs            afero.Fs
 	CrontabFile   string
 	CrontabBinary string
 	Username      string
@@ -54,7 +50,6 @@ func (s SchedulerCrond) Type() string                     { return constants.Sch
 func (s SchedulerCrond) Convert(_ string) SchedulerConfig { return s }
 
 type SchedulerSystemd struct {
-	Fs            afero.Fs
 	UnitTemplate  string
 	TimerTemplate string
 }


Test patch:
diff --git a/filesearch/filesearch_test.go b/filesearch/filesearch_test.go
index fbc00ee6..dac8d0c5 100644
--- a/filesearch/filesearch_test.go
+++ b/filesearch/filesearch_test.go
@@ -40,6 +40,8 @@ func TestMain(t *testing.M) {
 // ConfigDirs: [C:\ProgramData C:\Users\runneradmin\AppData\Roaming]
 // ApplicationDirs: [C:\Users\runneradmin\AppData\Roaming\Microsoft\Windows\Start Menu\Programs C:\ProgramData\Microsoft\Windows\Start Menu\Programs]
 func TestDefaultConfigDirs(t *testing.T) {
+	t.Parallel()
+
 	t.Log("ConfigHome:", xdg.ConfigHome)
 	t.Log("ConfigDirs:", xdg.ConfigDirs)
 	t.Log("ApplicationDirs:", xdg.ApplicationDirs)
@@ -192,6 +194,8 @@ func testLocations(t *testing.T) []testLocation {
 }
 
 func TestFindConfigurationFile(t *testing.T) {
+	t.Parallel()
+
 	// Work from a temporary directory
 	err := os.Chdir(os.TempDir())
 	require.NoError(t, err)
@@ -206,7 +210,7 @@ func TestFindConfigurationFile(t *testing.T) {
 		var err error
 		// Install empty config file
 		if location.realPath != "" {
-			err = fs.MkdirAll(location.realPath, 0700)
+			err = fs.MkdirAll(location.realPath, 0o700)
 			require.NoError(t, err)
 		}
 		file, err := fs.Create(filepath.Join(location.realPath, location.realFile))
@@ -230,12 +234,16 @@ func TestFindConfigurationFile(t *testing.T) {
 }
 
 func TestCannotFindConfigurationFile(t *testing.T) {
+	t.Parallel()
+
 	found, err := FindConfigurationFile("some_config_file")
 	assert.Empty(t, found)
 	assert.Error(t, err)
 }
 
 func TestFindResticBinary(t *testing.T) {
+	t.Parallel()
+
 	binary, err := FindResticBinary("some_other_name")
 	if binary != "" {
 		assert.True(t, strings.HasSuffix(binary, getResticBinaryName()))
@@ -246,6 +254,8 @@ func TestFindResticBinary(t *testing.T) {
 }
 
 func TestFindResticBinaryWithTilde(t *testing.T) {
+	t.Parallel()
+
 	if runtime.GOOS == "windows" {
 		t.Skip("not supported on Windows")
 		return
@@ -253,7 +263,7 @@ func TestFindResticBinaryWithTilde(t *testing.T) {
 	home, err := os.UserHomeDir()
 	require.NoError(t, err)
 
-	tempFile, err := afero.TempFile(fs, home, "TestFindResticBinaryWithTilde")
+	tempFile, err := afero.TempFile(fs, home, t.Name())
 	require.NoError(t, err)
 	tempFile.Close()
 	defer func() {
@@ -267,6 +277,8 @@ func TestFindResticBinaryWithTilde(t *testing.T) {
 }
 
 func TestShellExpand(t *testing.T) {
+	t.Parallel()
+
 	if runtime.GOOS == "windows" {
 		t.Skip("not supported on Windows")
 		return
@@ -290,6 +302,8 @@ func TestShellExpand(t *testing.T) {
 
 	for _, testItem := range testData {
 		t.Run(testItem.source, func(t *testing.T) {
+			t.Parallel()
+
 			result, err := ShellExpand(testItem.source)
 			require.NoError(t, err)
 			assert.Equal(t, testItem.expected, result)
@@ -298,6 +312,8 @@ func TestShellExpand(t *testing.T) {
 }
 
 func TestFindConfigurationIncludes(t *testing.T) {
+	t.Parallel()
+
 	testID := fmt.Sprintf("%d", uint32(time.Now().UnixNano()))
 	tempDir := os.TempDir()
 	files := []string{
@@ -309,7 +325,9 @@ func TestFindConfigurationIncludes(t *testing.T) {
 
 	for _, file := range files {
 		require.NoError(t, afero.WriteFile(fs, file, []byte{}, iofs.ModePerm))
-		defer fs.Remove(file) // defer stack is ok for cleanup
+		t.Cleanup(func() {
+			_ = fs.Remove(file)
+		})
 	}
 
 	testData := []struct {
@@ -335,6 +353,8 @@ func TestFindConfigurationIncludes(t *testing.T) {
 
 	for _, test := range testData {
 		t.Run(strings.Join(test.includes, ","), func(t *testing.T) {
+			t.Parallel()
+
 			result, err := FindConfigurationIncludes(files[0], test.includes)
 			if test.expected == nil {
 				assert.Nil(t, result)
@@ -350,3 +370,36 @@ func TestFindConfigurationIncludes(t *testing.T) {
 		})
 	}
 }
+
+func TestAddRootToRelativePaths(t *testing.T) {
+	t.Parallel()
+
+	if platform.IsWindows() {
+		t.Skip("not supported on Windows")
+	}
+
+	testCases := []struct {
+		root       string
+		inputPath  []string
+		outputPath []string
+	}{
+		{
+			root:       "",
+			inputPath:  []string{"", "dir", "~/user", "/root"},
+			outputPath: []string{"", "dir", "~/user", "/root"},
+		},
+		{
+			root:       "/home",
+			inputPath:  []string{"", "dir", "~/user", "/root"},
+			outputPath: []string{"/home", "/home/dir", "/home/user", "/root"},
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.root, func(t *testing.T) {
+			t.Parallel()
+
+			result := addRootToRelativePaths(testCase.root, testCase.inputPath)
+			assert.Equal(t, testCase.outputPath, result)
+		})
+	}
+}