14 Commits

Author SHA1 Message Date
5b91e64c26 fix: oops, missing comma. 🐛
Some checks failed
CI / build (push) Failing after 42s
2024-10-24 07:32:33 +13:00
02d93c1904 fix: Trying out PreRelease.
Some checks failed
CI / build (push) Failing after 40s
2024-10-24 07:31:03 +13:00
59acfec670 fix: Now run definers cleanup once after combining files.
All checks were successful
CI / build (push) Successful in 4m28s
Closes #7
2024-10-24 07:02:31 +13:00
26eabab106 fix: Invalid syntax on CREATE Trigger fixed.
All checks were successful
CI / build (push) Successful in 4m17s
Additional regex deployed to complete the transformation.
2024-09-26 16:08:22 +12:00
d44cad5ebd fix: Multiline regex for definers. 🐛
Prevent missing SQL statements.
Added "_" to expression
2024-09-05 14:15:42 +12:00
0cbfb81096 fix: Spelling update/fixed zip creation. 🐛
All checks were successful
CI / build (push) Successful in 3m12s
closes #3
closes #1
2024-09-03 11:08:27 +12:00
254183ceff chore: Updated build actions for win/nix 🧑‍💻 2024-09-03 11:06:24 +12:00
676ed11c3f chore: TODO removed as function created 💡 2024-09-02 22:49:15 +12:00
5d9be49f6c fix: Schema and Host transposed in output 🐛 2024-09-02 22:45:19 +12:00
3c5014e90b feat: Added initial support for .zip
All checks were successful
CI / build (push) Successful in 2m3s
2024-09-02 22:37:38 +12:00
1cef099a37 fix: Definers now removed from schema.sql closes #4 🐛 2024-09-02 16:53:04 +12:00
292d582a45 feat: Added build-win target to justfile 🔧
Allows build an executable for the windows platform.
2024-09-02 16:48:34 +12:00
8cc2fce727 feat: Added dump functionality 💩
All checks were successful
CI / build (push) Successful in 4m24s
2024-09-02 16:28:07 +12:00
924bf85a9e feat: Added utility for moving files 2024-09-02 16:25:27 +12:00
7 changed files with 484 additions and 2 deletions

View File

@@ -8,6 +8,7 @@
"options": {
"emojis": "true"
}
}
},
"maintainedVersion": "0-develop"
}
}

105
cmd/dump.go Normal file
View File

@@ -0,0 +1,105 @@
package cmd
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"hub.cybercinch.nz/guisea/gosqldump/internal/color"
"hub.cybercinch.nz/guisea/gosqldump/internal/dump"
"hub.cybercinch.nz/guisea/gosqldump/internal/icon"
"hub.cybercinch.nz/guisea/gosqldump/internal/style"
"time"
)
// dumpCmd represents the dump command
var dumpCmd = &cobra.Command{
Use: "dump",
Short: "Use this command to initiate dumping of MySQL/MariaDB Database",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
log.Info("dump called")
start := time.Now()
hostname, _ := cmd.Flags().GetString("host")
databaseName, _ := cmd.Flags().GetString("database")
//port, _ := cmd.Flags().GetInt("port")
// Retrieve user from config
user := viper.GetViper().GetString("mysql.user")
// Retrieve password from config
pass := viper.GetViper().GetString("mysql.password")
myDump := dump.NewClient(
dump.WithHostName(hostname),
dump.WithDatabaseName(databaseName),
dump.WithPort(3306),
dump.WithUserName(user),
dump.WithPassword(pass),
)
err := myDump.Dump()
if err != nil {
fmt.Printf("Error: %s", err)
return
}
fmt.Printf(
"%s Combining files\n\r",
style.Success(icon.Info),
)
filename, err := myDump.Combine()
if err != nil {
if err != nil {
fmt.Printf(
"%s could not combine backup file: %s\n\r",
style.Failure(icon.Cross),
lipgloss.NewStyle().Foreground(color.Red).Render(err.Error()),
)
return
}
return
}
fmt.Printf(
"%s Completed combining files\n\r",
style.Success(icon.Check),
)
fmt.Printf(
"%s Zipping file\n\r",
style.Success(icon.Info),
)
// Strip definers from the Dumpfile
myDump.RemoveDefiners(filename)
filename, _ = dump.ZipFile(filename)
timeElapsed := time.Since(start)
fmt.Printf(
"%s Dump file available at %s\r\nTook %s\n\r",
style.Success(icon.Check),
filename,
timeElapsed,
)
},
}
func init() {
rootCmd.AddCommand(dumpCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// dumpCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// dumpCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
dumpCmd.Flags().StringP("host", "H", "localhost", "database host")
dumpCmd.Flags().StringP("port", "p", "3306", "database port")
dumpCmd.Flags().StringP("database", "d", "~", "database name")
}

6
go.mod
View File

@@ -17,6 +17,12 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/kortschak/utter v1.7.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/paulvollmer/go-concatenate v0.1.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/rwtodd/Go.Sed v0.0.0-20240405174034-bb8ed5da0fd0 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect

8
go.sum
View File

@@ -12,6 +12,7 @@ github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqo
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -38,6 +39,8 @@ github.com/ivanpirog/coloredcobra v1.0.1 h1:aURSdEmlR90/tSiWS0dMjdwOvCVUeYLfltLf
github.com/ivanpirog/coloredcobra v1.0.1/go.mod h1:iho4nEKcnwZFiniGSdcgdvRgZNjxm+h20acv8vqmN6Q=
github.com/ka-weihe/fast-levenshtein v0.0.0-20201227151214-4c99ee36a1ba h1:keZ4vJpYOVm6yrjLzZ6QgozbEBaT0GjfH30ihbO67+4=
github.com/ka-weihe/fast-levenshtein v0.0.0-20201227151214-4c99ee36a1ba/go.mod h1:kaXTPU4xitQT0rfT7/i9O9Gm8acSh3DXr0p4y3vKqiE=
github.com/kortschak/utter v1.7.0 h1:6NKMynvGUyqfeMTawfah4zyInlrgwzjkDAHrT+skx/w=
github.com/kortschak/utter v1.7.0/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -60,8 +63,11 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/paulvollmer/go-concatenate v0.1.0 h1:LZ9CdhNXIo6JKW7UGTYMzR1/wRsd0Cv6HYaL1oumxrg=
github.com/paulvollmer/go-concatenate v0.1.0/go.mod h1:U2NPShgMlzNi1oyK4Uvh/QUplGeiWGCgEtFPdReflwo=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -70,6 +76,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwtodd/Go.Sed v0.0.0-20240405174034-bb8ed5da0fd0 h1:Sm5QvnDuFhkajkdjAHX51h+gyuv+LmkjX//zjpZwIvA=
github.com/rwtodd/Go.Sed v0.0.0-20240405174034-bb8ed5da0fd0/go.mod h1:c6qgHcSUeSISur4+Kcf3WYTvpL07S8eAsoP40hDiQ1I=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=

300
internal/dump/dump.go Normal file
View File

@@ -0,0 +1,300 @@
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",
"--compact",
"--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
}
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",
"--compact",
"--result-file", filepath.Join(c.storagePath, c.databaseName+"-data.sql"),
c.databaseName).Run()
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",
"--compact",
"--result-file", filepath.Join(c.storagePath, c.databaseName+"-routines.sql"),
c.databaseName).Run()
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 (c *Client) RemoveDefiners(filename string) {
expressions := make([]string, 1)
expressions[0] = `s/DEFINER=[^ ]* / /g`
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
}

58
internal/util/files.go Normal file
View File

@@ -0,0 +1,58 @@
package util
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func MoveFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("couldn't open source file: %s", err)
}
out, err := os.Create(dst)
if err != nil {
in.Close()
return fmt.Errorf("couldn't open dest file: %s", err)
}
defer out.Close()
_, err = io.Copy(out, in)
in.Close()
if err != nil {
return fmt.Errorf("writing to output file failed: %s", err)
}
err = out.Sync()
if err != nil {
return fmt.Errorf("sync error: %s", err)
}
si, err := os.Stat(src)
if err != nil {
return fmt.Errorf("stat error: %s", err)
}
err = os.Chmod(dst, si.Mode())
if err != nil {
return fmt.Errorf("chmod error: %s", err)
}
//err = in.Close()
//if err != nil {
// return fmt.Errorf("closing file failed: %s", err)
//}
//time.Sleep(time.Second * 10)
err = os.Remove(src)
if err != nil {
return fmt.Errorf("failed removing original file: %s", err)
}
return nil
}
func FileNameWithoutExt(fileName string) string {
return strings.TrimSuffix(fileName, filepath.Ext(fileName))
}

View File

@@ -8,7 +8,11 @@ install:
build:
@echo -n "Building app ... "
@go build {{flags}} -o bin/ && echo "OK" || echo "FAILED"
@go build {{flags}} -o bin/gosqldump ./main.go && echo "OK" || echo "FAILED"
build-win:
@echo -n "Building app for windows ... "
@GOOS=windows GOARCH=amd64 go build {{flags}} -o bin/gosqldump.exe ./main.go && echo "OK" || echo "FAILED"
update:
go get -u
go mod tidy -v