Repository: gobackup/gobackup

Issue:
有错误
![Image](https://github.com/user-attachments/assets/04acd8d2-6855-4cc8-8ad9-f46011c342bc)
每次备份都提示这个报错

Discussion:
@hanqingwu: 看下你的备份配置@kongchenjiao: ```yml
web:
  username: gobackup
  password: 123456
models:
  my_backup:
    before_script: |
      echo "Before script"
    after_script: |
      echo "After script"
    schedule:
      # At 04:05 on Sunday.
      cron: "5 4 * * sun"
    storages:
      local:
        type: local
        keep: 6
        path: /data/backup
    databases:
      mysql:
        type: mysql
        host: 192.168.1.160
        port: 3306
        database: www
        username: root
        password: 123
      redis1:
        type: redis
        mode: sync
        rdb_path: /var/lib/redis/dump.rdb
        invoke_save: true
    archive:
      includes:
      - /etc/nginx/nginx.conf
      - /etc/redis/redis.conf
    compress_with:
      type: tgz
```@huacnlee: 看起来像是磁盘没权限或者由于什么其他的原因，写不进去。你根据这个检查一下呢@kongchenjiao: 虽然有提示这个报错，但是每次备份的文件都是存在的，解压出来的文件也都是正常的，不过有一点就是使用的系统是uos@hanqingwu: @kongchenjiao ， 你看下手动执行        echo "Before script"  会不会报错，@kongchenjiao: > [@kongchenjiao](https://github.com/kongchenjiao) ， 你看下手动执行 echo "Before script" 会不会报错，

有新的发现
通过web界面点击执行备份会提示这个报错
手动执行./gobackup perform备份不会提示这个报错@hanqingwu: 那比较可能是你的web 运行环境的权限问题。 看下是不是跟控制台相同权限的。@kongchenjiao: > 那比较可能是你的web 运行环境的权限问题。 看下是不是跟控制台相同权限的。

我似乎查到问题了，当./gobackup run前台运行程序时web界面备份不会提示报错，当./gobackup start后台运行时web界面会提示报错@kongchenjiao: > > 那比较可能是你的web 运行环境的权限问题。 看下是不是跟控制台相同权限的。
> 
> 我似乎查到问题了，当./gobackup run前台运行程序时web界面备份不会提示报错，当./gobackup start后台运行时web界面会提示报错

我暂时使用nohup ./gobackup run & 解决这个问题了@huacnlee: https://github.com/gobackup/gobackup/releases/tag/v2.15.2

Solution patch:
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/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/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)
 		}



Test patch:
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")
+}