package dump import ( "archive/zip" "fmt" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" "github.com/paulvollmer/go-concatenate" "github.com/rwtodd/Go.Sed/sed" "hub.cybercinch.nz/guisea/gosqldump/internal/color" "hub.cybercinch.nz/guisea/gosqldump/internal/icon" "hub.cybercinch.nz/guisea/gosqldump/internal/style" "hub.cybercinch.nz/guisea/gosqldump/internal/util" "io" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" ) // Client represents our mysqldump client. type Client struct { executable string hostname string port int databaseName string username string password string storagePath string } // Option is a functional option type that allows us to configure the Client. type Option func(*Client) func NewClient(options ...Option) *Client { client := &Client{ executable: "mysqldump", } switch runtime.GOOS { case "darwin": client.storagePath = filepath.Join("/tmp") case "linux": client.storagePath = filepath.Join("/tmp") case "windows": client.storagePath = filepath.Join("C:\\", "Temp") } err := os.MkdirAll(client.storagePath, os.ModePerm) if err != nil { log.Debug(err) } // Apply all the functional options to configure the client. for _, opt := range options { opt(client) } return client } // WithDatabaseName sets the Database Name to work with func WithDatabaseName(databaseName string) Option { return func(c *Client) { c.databaseName = databaseName } } // WithHostName is a functional option to set the Database Hostname. func WithHostName(hostName string) Option { return func(c *Client) { c.hostname = hostName } } // WithPort is a functional option to set the Database Hostname. func WithPort(port int) Option { return func(c *Client) { c.port = port } } // WithUserName is a functional option to set the Database Hostname. func WithUserName(userName string) Option { return func(c *Client) { c.username = userName } } // WithPassword is a functional option to set the Database Hostname. func WithPassword(password string) Option { return func(c *Client) { c.password = password } } func (c *Client) Dump() error { // Construct schema output fmt.Printf("%s Dumping schema %s from %s\n\r", style.Success(icon.Info), lipgloss.NewStyle().Foreground(color.Yellow).Render(fmt.Sprint(c.databaseName)), lipgloss.NewStyle().Foreground(color.Purple).Render(c.hostname), ) f, _ := os.OpenFile(filepath.Join(c.storagePath, c.databaseName+"-keys.sql"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) _, err := f.WriteString("SET FOREIGN_KEY_CHECKS=0;\n") if err != nil { return err } f.Close() response, err := exec.Command(c.executable, "--host="+c.hostname, "--port="+strconv.Itoa(c.port), "--user="+c.username, "--password="+c.password, "--no-create-db", "--no-data", "--skip-triggers", "--result-file", filepath.Join(c.storagePath, c.databaseName+"-schema.sql"), c.databaseName).CombinedOutput() if err != nil { fmt.Printf( "%s mysqldump error: %s\n\r", style.Failure(icon.Cross), lipgloss.NewStyle().Foreground(color.Purple).Render(string(response)), ) return err } removeDefiners(filepath.Join(c.storagePath, c.databaseName+"-schema.sql")) fmt.Printf( "%s Done dumping schema %s from %s\n\r", style.Success(icon.Check), lipgloss.NewStyle().Foreground(color.Yellow).Render(fmt.Sprint(c.databaseName)), lipgloss.NewStyle().Foreground(color.Purple).Render(c.hostname), ) // Construct data output fmt.Printf("%s Dumping data %s from %s\n\r", style.Success(icon.Info), lipgloss.NewStyle().Foreground(color.Yellow).Render(fmt.Sprint(c.databaseName)), lipgloss.NewStyle().Foreground(color.Purple).Render(c.hostname), ) _ = exec.Command(c.executable, "--host="+c.hostname, "--port="+strconv.Itoa(c.port), "--user="+c.username, "--password="+c.password, "--no-create-db", "--no-create-info", "--skip-triggers", "--result-file", filepath.Join(c.storagePath, c.databaseName+"-data.sql"), c.databaseName).Run() removeDefiners(filepath.Join(c.storagePath, c.databaseName+"-data.sql")) fmt.Printf( "%s Done dumping data %s from %s\n\r", style.Success(icon.Check), lipgloss.NewStyle().Foreground(color.Yellow).Render(fmt.Sprint(c.databaseName)), lipgloss.NewStyle().Foreground(color.Purple).Render(c.hostname), ) // Construct routines/triggers output fmt.Printf("%s Dumping routines %s from %s\n\r", style.Success(icon.Info), lipgloss.NewStyle().Foreground(color.Yellow).Render(fmt.Sprint(c.databaseName)), lipgloss.NewStyle().Foreground(color.Purple).Render(c.hostname), ) _ = exec.Command(c.executable, "--host="+c.hostname, "--port="+strconv.Itoa(c.port), "--user="+c.username, "--password="+c.password, "--no-create-db", "--no-create-info", "--no-data", "--triggers", "--routines", "--result-file", filepath.Join(c.storagePath, c.databaseName+"-routines.sql"), c.databaseName).Run() removeDefiners(filepath.Join(c.storagePath, c.databaseName+"-routines.sql")) fmt.Printf( "%s Done dumping routines %s from %s\n\r", style.Success(icon.Check), lipgloss.NewStyle().Foreground(color.Purple).Render(c.hostname), lipgloss.NewStyle().Foreground(color.Yellow).Render(fmt.Sprint(c.databaseName)), ) return nil } func removeDefiners(filename string) { // Regex 1: .*(DEFINER=[a-zA-Z0-9\x60%@]+).* (Used by procedures/funcs) // Regex 2: .*(\/\*\!50003.*!50003+).* (Used by triggers) // Regex 3: (\/\*\!50013.*DEFINER \*\/) (Used in schema) expressions := make([]string, 3) expressions[0] = `s/(^.*)(.DEFINER=[a-zA-Z0-9_\x60%@]+)(.*)/$1$3/gm` expressions[1] = `s/(.*)(\/\*\!50003.*!50003+)(.*)/$1$3/gm` expressions[2] = `s/(\/\*\!50013.*DEFINER \*\/)//gm` if !strings.Contains(filename, "data") { for _, re := range expressions { inputFile, err := os.Open(filename) engine, err := sed.New(strings.NewReader(re)) if err != nil { } outputFile, err := os.OpenFile(filename+".new", os.O_WRONLY|os.O_CREATE, 0666) _, _ = io.Copy(outputFile, engine.Wrap(inputFile)) _ = inputFile.Close() _ = outputFile.Close() err = util.MoveFile(filename+".new", filename) if err != nil { fmt.Printf( "%s could not move file: %s\n\r", style.Failure(icon.Cross), lipgloss.NewStyle().Foreground(color.Purple).Render(err.Error()), ) return } } } } func (c *Client) Combine() (string, error) { var files [4]string files[0] = "keys" files[1] = "schema" files[2] = "data" files[3] = "routines" filepath.Join(c.storagePath, c.databaseName+"-backup.sql") err := concatenate.FilesToFile(filepath.Join(c.storagePath, c.databaseName+"-backup.sql"), 0666, "", filepath.Join(c.storagePath, c.databaseName+"-"+files[0]+".sql"), filepath.Join(c.storagePath, c.databaseName+"-"+files[1]+".sql"), filepath.Join(c.storagePath, c.databaseName+"-"+files[2]+".sql"), filepath.Join(c.storagePath, c.databaseName+"-"+files[3]+".sql"), ) if err != nil { return "", err } for _, file := range files { err = os.Remove(filepath.Join(c.storagePath, c.databaseName+"-"+file+`.sql`)) if err != nil { return "", err } } return filepath.Join(c.storagePath, c.databaseName+"-backup.sql"), nil } func ZipFile(filename string) (string, error) { //Create a new zip archive and named archive.zip archive, err := os.Create(util.FileNameWithoutExt(filename) + ".zip") if err != nil { panic(err) // this is to catch errors if any } defer archive.Close() _, file := filepath.Split(filename) //Create a new zip writer zipWriter := zip.NewWriter(archive) fmt.Printf( "%s Opening .sql file\n\r", style.Success(icon.Info), ) f1, err := os.Open(filename) if err != nil { panic(err) } defer f1.Close() fmt.Printf( "%s Adding file to archive\n\r", style.Success(icon.Info), ) w1, err := zipWriter.Create(file) if err != nil { panic(err) } if _, err := io.Copy(w1, f1); err != nil { panic(err) } fmt.Printf( "%s Closing archive\n\r", style.Success(icon.Info), ) zipWriter.Close() f1.Close() os.Remove(filename) return util.FileNameWithoutExt(filename) + ".zip", nil }