Repository: gobackup/gobackup

Issue:
压缩包是否可以增加一个压缩等级的选项，以减少备份文件的体积。
或者可以设置其他压缩方式，减少备份包的文件体积

Discussion:
@huacnlee: 可以试试这个 PostgreSQL 支持了

https://github.com/gobackup/gobackup/pull/280

https://github.com/gobackup/gobackup/releases/tag/v2.14.0@lm93129: > 可以试试这个 PostgreSQL 支持了
> 
> [#280](https://github.com/gobackup/gobackup/pull/280)
> 
> https://github.com/gobackup/gobackup/releases/tag/v2.14.0

我觉得是不是可以再[compressor](https://github.com/gobackup/gobackup/tree/main/compressor)中增加其他压缩方式，例如使用archive/zip包实现的压缩方式，这样可以使其他需要压缩的，有压缩等级@huacnlee: Try https://gobackup.github.io/configuration/compressor/tar#speedup-with-pigz@lm93129: > Try https://gobackup.github.io/configuration/compressor/tar#speedup-with-pigz

使用pigz可以实现该效果

Solution patch:
diff --git a/.github/workflows/close-slate-issues.yml b/.github/workflows/close-slate-issues.yml
new file mode 100644
index 0000000..283cb40
--- /dev/null
+++ b/.github/workflows/close-slate-issues.yml
@@ -0,0 +1,30 @@
+name: "Close Stale Issues"
+permissions:
+  contents: write # only for delete-branch option
+  issues: write
+  pull-requests: write
+
+on:
+  schedule:
+    - cron: "12 3 * * 4"
+  workflow_dispatch:
+
+jobs:
+  stale:
+    if: github.repository_owner == 'gobackup'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/stale@v9
+        with:
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          close-issue-message: >
+            This issue was closed due to inactivity at least 180 days. If you still encounter this issue, please open a new issue and link to this issue.
+
+            Thank you.
+          days-before-stale: 180
+          days-before-close: 7
+          operations-per-run: 1000
+          ascending: true
+          enable-statistics: true
+          stale-issue-label: "stale"
+          exempt-issue-labels: "enhancement"
diff --git a/compressor/tar.go b/compressor/tar.go
index c9fde96..63228da 100644
--- a/compressor/tar.go
+++ b/compressor/tar.go
@@ -40,5 +40,10 @@ func (tar *Tar) options() (opts []string) {
 	}
 	opts = append(opts, "-cf")
 
+	args := tar.viper.GetString("args")
+	if len(args) > 0 {
+		opts = append(opts, args)
+	}
+
 	return
 }
diff --git a/database/base.go b/database/base.go
index 39f8bbe..85096a1 100644
--- a/database/base.go
+++ b/database/base.go
@@ -3,9 +3,7 @@ package database
 import (
 	"fmt"
 	"path"
-	"strings"
 
-	"github.com/google/shlex"
 	"github.com/spf13/viper"
 
 	"github.com/gobackup/gobackup/config"
@@ -51,27 +49,12 @@ func runHook(action, script string) error {
 		return nil
 	}
 	logger.Infof("Run %s", action)
-	ignoreError := strings.HasPrefix(script, "-")
-	script = strings.TrimPrefix(script, "-")
-	c, err := shlex.Split(script)
-	if err != nil {
-		if ignoreError {
-			logger.Infof("Skip %s with error: %v", action, err)
-		} else {
-			return err
-		}
-	} else {
-		if _, err := helper.Exec(c[0], c[1:]...); err != nil {
-			if ignoreError {
-				logger.Infof("Run %s failed: %v, ignore it", action, err)
-			} else {
-				return fmt.Errorf("Run %s failed: %v", action, err)
-			}
-		} else {
-			logger.Infof("Run %s succeeded", action)
-		}
+
+	if _, err := helper.ExecScriptWithStdio(script, true); err != nil {
+		return fmt.Errorf("Run %s failed: %v", action, err)
 	}
 
+	logger.Infof("Run %s succeeded", action)
 	return nil
 }
 
diff --git a/database/redis.go b/database/redis.go
index d844cdb..4f29b7c 100644
--- a/database/redis.go
+++ b/database/redis.go
@@ -3,7 +3,6 @@ package database
 import (
 	"fmt"
 	"path"
-	"regexp"
 	"strings"
 
 	"github.com/gobackup/gobackup/helper"
@@ -147,11 +146,7 @@ func (db *Redis) trySave() error {
 	if err != nil {
 		return fmt.Errorf("redis-cli SAVE failed %s", err)
 	}
-
-	if !regexp.MustCompile("OK$").MatchString(strings.TrimSpace(out)) {
-		return fmt.Errorf(`failed to invoke the "SAVE" command Response was: %s`, out)
-	}
-
+	logger.Info("Perform save result:", out)
 	return nil
 }
 
diff --git a/gobackup_test.yml b/gobackup_test.yml
index 3f753a2..415f2ff 100644
--- a/gobackup_test.yml
+++ b/gobackup_test.yml
@@ -130,9 +130,13 @@ models:
       password: 123456
     notifiers:
     before_script: |
-      echo "before_script invoked."
+      echo before_script invoked.
+      ls -lh | wc -l
+      echo This is before_script second line.
     after_script: |
-      echo "after_script invoked."
+      echo after_script invoked.
+      ls -lh | wc -l
+      echo This is after_script second line.
     storages:
       local:
         type: local
diff --git a/helper/exec.go b/helper/exec.go
index e20fea1..a2a792b 100644
--- a/helper/exec.go
+++ b/helper/exec.go
@@ -6,10 +6,12 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
+	"path"
 	"regexp"
 	"strings"
 
 	"github.com/gobackup/gobackup/logger"
+	"github.com/google/uuid"
 )
 
 var (
@@ -59,3 +61,29 @@ func ExecWithStdio(command string, stdout bool, args ...string) (output string,
 
 	return
 }
+
+// Execute multiple line script with stdio
+func ExecScriptWithStdio(script string, stdout bool) (string, error) {
+	tmpFileName, _ := uuid.NewUUID()
+	tmpFile := path.Join(os.TempDir(), tmpFileName.String())
+
+	f, err := os.Create(tmpFile)
+	if err != nil {
+		return "", err
+	}
+	f.WriteString(script)
+	err = os.Chmod(tmpFile, 0755)
+	if err != nil {
+		return "", err
+	}
+
+	defer f.Close()
+	defer os.Remove(tmpFile)
+
+	return ExecWithStdio("sh", stdout, tmpFile)
+}
+
+// Execute multiple line script
+func ExecScript(script string) (output string, err error) {
+	return ExecScriptWithStdio(script, false)
+}
diff --git a/install b/install
index 58d83fa..6a92498 100755
--- a/install
+++ b/install
@@ -2,21 +2,24 @@
 
 set -u
 
-type curl > /dev/null || { echo "curl: not found"; exit 1; }
+type curl >/dev/null || {
+  echo "curl: not found"
+  exit 1
+}
 
 set -e
 
 get_latest_release() {
-  local repo="$1"
-	curl -sSL "https://api.github.com/repos/${repo}/releases/latest" | \
+  repo="$1"
+  curl -sSL "https://api.github.com/repos/${repo}/releases/latest" |
     awk 'BEGIN{FS=": |,|\""}; /tag_name/{print $5}'
 }
 
 repo="gobackup/gobackup"
-version="$(get_latest_release "${repo}")"  # v1.2.0
+version="$(get_latest_release "${repo}")" # v1.2.0
 
 # if args has version override it and not eq "latest"
-if test $# -eq 1; then
+if test "$#" -eq 1; then
   if test "$1" != "latest"; then
     version="$1"
 
@@ -24,7 +27,7 @@ if test $# -eq 1; then
   fi
 fi
 
-platform="$(uname | tr "[A-Z]" "[a-z]")"  # Linux => linux
+platform="$(uname | tr "[:upper:]" "[:lower:]")"                    # Linux => linux
 arch="$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/')" # x86_64 => amd64, aarch64 => arm64
 package="gobackup-${platform}-${arch}.tar.gz"
 package_url="https://github.com/${repo}/releases/download/${version}/${package}"
@@ -33,6 +36,7 @@ dest_dir="/usr/local/bin"
 bin_path="${dest_dir}/${bin}"
 tmp_dir="$(mktemp -d)"
 
+# shellcheck disable=SC2064
 trap "rm -r ${tmp_dir}" EXIT
 
 if test -e "${bin_path}"; then
@@ -47,7 +51,7 @@ fi
 cd "${tmp_dir}"
 curl -sSL "${package_url}" | tar xzf -
 
-if test $(id -u) -eq 0; then
+if test "$(id -u)" -eq 0; then
   mv "${bin}" "${dest_dir}"
 else
   sudo mv "${bin}" "${dest_dir}"
diff --git a/logger/logger.go b/logger/logger.go
index b723884..766dba2 100644
--- a/logger/logger.go
+++ b/logger/logger.go
@@ -46,8 +46,13 @@ func init() {
 }
 
 func SetLogger(logPath string) {
-	logfile, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
-	multi := io.MultiWriter(logfile, os.Stdout)
+	writers := make([]io.Writer, 0)
+	writers = append(writers, os.Stdout)
+	logfile, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+	if err == nil {
+		writers = append(writers, logfile)
+	}
+	multi := io.MultiWriter(writers...)
 	_myLog = log.New(&writer{multi, TimeFormat}, "", 0)
 	sharedLogger = newLogger()
 }
diff --git a/model/model.go b/model/model.go
index 1f79919..9a38b43 100644
--- a/model/model.go
+++ b/model/model.go
@@ -88,7 +88,7 @@ func (m Model) before() {
 	// Execute before_script
 	if len(m.Config.BeforeScript) > 0 {
 		logger.Info("Executing before_script...")
-		_, err := helper.ExecWithStdio(m.Config.BeforeScript, true)
+		_, err := helper.ExecScriptWithStdio(m.Config.BeforeScript, true)
 		if err != nil {
 			logger.Error(err)
 		}
@@ -111,7 +111,7 @@ func (m Model) after() {
 	// Execute after_script
 	if len(m.Config.AfterScript) > 0 {
 		logger.Info("Executing after_script...")
-		_, err := helper.ExecWithStdio(m.Config.AfterScript, true)
+		_, err := helper.ExecScriptWithStdio(m.Config.AfterScript, true)
 		if err != nil {
 			logger.Error(err)
 		}
diff --git a/notifier/base.go b/notifier/base.go
index 6ca4589..b3f4677 100644
--- a/notifier/base.go
+++ b/notifier/base.go
@@ -66,7 +66,9 @@ func newNotifier(name string, config config.SubConfig) (Notifier, *Base, error)
 		return NewWxWork(base), base, nil
 	case "googlechat":
 	        return NewGoogleChat(base), base, nil
- 	}
+	case "healthchecks":
+		return NewHealthchecks(base), base, nil
+	}
 
 	return nil, nil, fmt.Errorf("Notifier: %s is not supported", name)
 }
diff --git a/notifier/healthchecks.go b/notifier/healthchecks.go
new file mode 100644
index 0000000..acf3f65
--- /dev/null
+++ b/notifier/healthchecks.go
@@ -0,0 +1,100 @@
+package notifier
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/gobackup/gobackup/logger"
+	"io"
+	"net/http"
+	"strings"
+)
+
+type Healthchecks struct {
+	Base
+	Service         string
+	successEndpoint string
+	failureEndpoint string
+	checkResult     func(status int, responseBody []byte) error
+}
+
+// NewHealthchecks
+// type: healthchecks
+// url: https://healthchecks.io/ping/e9ef3495-aa2b-4e8b-8927-a7138dd6c04c
+func NewHealthchecks(base *Base) *Healthchecks {
+	url := base.viper.GetString("url")
+
+	return &Healthchecks{
+		Base:            *base,
+		Service:         "Healthchecks",
+		successEndpoint: url,
+		failureEndpoint: url + "/fail",
+		checkResult: func(status int, responseBody []byte) error {
+			if status == 200 {
+				return nil
+			}
+
+			return fmt.Errorf("status: %d, body: %s", status, string(responseBody))
+		},
+	}
+}
+
+func (s *Healthchecks) getLogger() logger.Logger {
+	return logger.Tag(fmt.Sprintf("Notifier: %s", s.Service))
+}
+
+func (s *Healthchecks) notify(title string, message string) error {
+	logger := s.getLogger()
+	url := s.failureEndpoint
+	if strings.Contains(title, "OK") {
+		url = s.successEndpoint
+	}
+
+	payload, err := json.Marshal(map[string]string{
+		"title":   title,
+		"message": message,
+	})
+
+	logger.Infof("Send notification to %s...", url)
+	req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(string(payload)))
+	if err != nil {
+		logger.Error(err)
+		return err
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		logger.Error(err)
+		return err
+	}
+	defer func(Body io.ReadCloser) {
+		err = Body.Close()
+		if err != nil {
+			logger.Error(err)
+		}
+	}(resp.Body)
+
+	var body []byte
+	if resp.Body != nil {
+		body, err = io.ReadAll(resp.Body)
+		if err != nil {
+			return err
+		}
+	}
+
+	if s.checkResult != nil {
+		err = s.checkResult(resp.StatusCode, body)
+		if err != nil {
+			logger.Error(err)
+			return nil
+		}
+	} else {
+		logger.Infof("Response body: %s", string(body))
+	}
+
+	logger.Info("Notification sent.")
+
+	return nil
+}



Test patch:
diff --git a/compressor/tar_test.go b/compressor/tar_test.go
index dff1fbe..7ed4953 100644
--- a/compressor/tar_test.go
+++ b/compressor/tar_test.go
@@ -3,12 +3,25 @@ package compressor
 import (
 	"testing"
 
+	"github.com/gobackup/gobackup/config"
 	"github.com/gobackup/gobackup/helper"
 	"github.com/longbridgeapp/assert"
+	"github.com/spf13/viper"
 )
 
 func TestTar_options(t *testing.T) {
-	tar := &Tar{}
+	viper := viper.New()
+	viper.Set("args", "--foo --bar --dar")
+	base := newBase(config.ModelConfig{
+		CompressWith: config.SubConfig{
+			Type:  "compress_with",
+			Name:  "tar",
+			Viper: viper,
+		},
+	},
+	)
+
+	tar := &Tar{base}
 	opts := tar.options()
 	if helper.IsGnuTar {
 		assert.Equal(t, opts[0], "--ignore-failed-read")
@@ -17,6 +30,7 @@ func TestTar_options(t *testing.T) {
 	} else {
 		assert.Equal(t, opts[0], "-a")
 		assert.Equal(t, opts[1], "-cf")
+		assert.Equal(t, opts[2], "--foo --bar --dar")
 	}
 
 }
diff --git a/helper/exec_test.go b/helper/exec_test.go
index 79b1196..130a693 100644
--- a/helper/exec_test.go
+++ b/helper/exec_test.go
@@ -38,3 +38,19 @@ func TestExecWithStdio(t *testing.T) {
 	assert.Nil(t, err)
 	assert.Empty(t, out)
 }
+
+func TestExecScriptWithStdio(t *testing.T) {
+	out, err := ExecScriptWithStdio("head -n1 ./exec_test.go\n# This is a comment\nhead -n1 ./exec_test.go | wc -c | sed 's/^[[:space:]]*//'\necho hello world\necho foobar", false)
+	assert.Nil(t, err)
+	assert.Equal(t, out, "package helper\n15\nhello world\nfoobar")
+
+	out, err = ExecScriptWithStdio("head -n1", true)
+	assert.Nil(t, err)
+	assert.Empty(t, out)
+}
+
+func TestExecScript(t *testing.T) {
+	out, err := ExecScript("head -n1 ./exec_test.go\n# This is a comment\necho hello world")
+	assert.Nil(t, err)
+	assert.Equal(t, out, "package helper\nhello world")
+}
diff --git a/notifier/healthchecks_test.go b/notifier/healthchecks_test.go
new file mode 100644
index 0000000..d1f2588
--- /dev/null
+++ b/notifier/healthchecks_test.go
@@ -0,0 +1,24 @@
+package notifier
+
+import (
+	"testing"
+
+	"github.com/longbridgeapp/assert"
+	"github.com/spf13/viper"
+)
+
+func Test_Healthchecks(t *testing.T) {
+	base := &Base{
+		viper: viper.New(),
+	}
+
+	s := NewHealthchecks(base)
+	assert.Equal(t, "Healthchecks", s.Service)
+
+	err := s.checkResult(200, []byte(`{"status":"ok"}`))
+	assert.NoError(t, err)
+
+	respBody := `{"status":"error"}`
+	err = s.checkResult(403, []byte(respBody))
+	assert.EqualError(t, err, "status: 403, body: "+respBody)
+}