diff --git a/.gitignore b/.gitignore index 3b735ec..cf84760 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ # Go workspace file go.work + +logs +.testdata diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8da59f6 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +build: + go build -o easybackup cmd/* + +install: + mv easybackup ~/go/bin + +clean: + rm -f easybackup diff --git a/README.md b/README.md index f3d0473..7f04028 100644 --- a/README.md +++ b/README.md @@ -1 +1,90 @@ -# EasyBackup \ No newline at end of file +# EasyBackup + +EasyBackup is a backup tool that currently supports xtrabackup. + + +## Install +```bash +$ make && make install +$ easybackup --help +Usage: + easybackup [flags] + easybackup [command] + +Available Commands: + backup Take a backup + completion Generate the autocompletion script for the specified shell + help Help about any command + init Init a repository + list List backup sets in repository + restore Restore a database from backupset + +Flags: + -h, --help help for easybackup + +Use "easybackup [command] --help" for more information about a command. +``` + +## Usage + +### Backup +1、Create a json file for backup +```bash +$ cat << EOF > config.json +{ + "identifer": "instanceName", + "version": "8.0.28", + "login_path": "MYDB8028", + "db_hostname": "127.0.0.1", + "db_user": "mysql", + "throttle": 400, + "try_compress": true, + "bin_path": "/usr/local/xtrabackup/8.0.28/bin", + "data_path": "/data/mysql/8.0.28", + "backup_user": "backupuser", + "backup_hostname": "127.0.0.1" +} +EOF +``` + +2、Init a repository in path `/data/backup`, the repository name is `repo1` +```bash +$ easybackup init -f config.json -p /data/backup -n repo1 +``` + +3、Take a full backup and check it +```bash +$ easybackup backup -p /data/backup/repo1 -t full +``` + +```bash +$ easybackup list backupset -p /data/backup/repo1 +BackupTime | Id | Type | FromLSN | ToLSN | Size(Kb) +2024-01-06 22:58:07 | 25070168-099e-4f6d-9274-bad6edf71ac4 | full | 0 | 18166699 | 3505 +``` + +4、Take a incr backup and check it +```bash +$ easybackup backup -p /data/backup/repo1 -t incr +``` + +```bash +$ easybackup list backupset -p /data/backup/repo1 +BackupTime | Id | Type | FromLSN | ToLSN | Size(Kb) +2024-01-06 22:58:07 | 25070168-099e-4f6d-9274-bad6edf71ac4 | full | 0 | 18166699 | 3505 +2024-01-06 23:00:58 | 3d76a621-e9bf-4a67-b663-143923e392ea | incr | 18166699 | 18166719 | 212 +``` + +### Restore +1、Restore database from backupset, the target path is `/data/restore/instance01` +```bash +$ easybackup restore -p /data/backup/repo1 -m /usr/local/mysql/8.0.28 -t /data/restore/instance01 -i 3d76a621-e9bf-4a67-b663-143923e392ea +``` + +2、Check it, the database is started and port is `36627` ! +```bash +$ ps -ef|grep mysql |grep /data/restore/instance01 +root 87748 1 0 23:30 ? 00:00:00 sudo -u mysql /usr/local/mysql/8.0.28/bin/mysqld_safe --defaults-file=/data/restore/instance01/my.cnf +mysql 87750 87748 0 23:30 ? 00:00:00 /bin/sh /usr/local/mysql/8.0.28/bin/mysqld_safe --defaults-file=/data/restore/instance01/my.cnf +mysql 87976 87750 0 23:30 ? 00:00:01 /usr/local/mysql/8.0.28/bin/mysqld --defaults-file=/data/restore/instance01/my.cnf --basedir=/usr/local/mysql/8.0.28 --datadir=/data/restore/instance01/25070168-099e-4f6d-9274-bad6edf71ac4 --plugin-dir=/usr/local/mysql/8.0.28/lib/plugin --log-error=/data/restore/instance01/mysql.err --pid-file=/data/restore/instance01/mysql.pid --socket=/data/restore/instance01/mysql.sock --port=36627 +``` diff --git a/cmd/cmdBackup.go b/cmd/cmdBackup.go new file mode 100644 index 0000000..cd376ec --- /dev/null +++ b/cmd/cmdBackup.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "os" + + "github.com/skyline93/mysql-xtrabackup/internal/mysql" + "github.com/skyline93/mysql-xtrabackup/internal/repository" + "github.com/spf13/cobra" +) + +var cmdBackup = &cobra.Command{ + Use: "backup -p /data/backup/repo1 -t full", + Short: "Take a backup", + Run: func(cmd *cobra.Command, args []string) { + repo := repository.Repository{} + if err := repository.LoadRepository(&repo, backupOptions.RepoPath); err != nil { + fmt.Printf("load repo error: %s", err) + os.Exit(1) + } + + backuper := mysql.NewBackuper() + if err := backuper.Backup(&repo, backupOptions.BackupType); err != nil { + fmt.Printf("backup failed error: %s", err) + os.Exit(1) + } + }, +} + +type BackupOptions struct { + BackupType string + RepoPath string +} + +var backupOptions BackupOptions + +func init() { + cmdRoot.AddCommand(cmdBackup) + + f := cmdBackup.Flags() + f.StringVarP(&backupOptions.BackupType, "backup_type", "t", "full", "backup type") + f.StringVarP(&backupOptions.RepoPath, "repo_path", "p", "", "repo path") + + cmdBackup.MarkFlagRequired("backup_type") + cmdBackup.MarkFlagRequired("repo_path") +} diff --git a/cmd/cmdInit.go b/cmd/cmdInit.go new file mode 100644 index 0000000..5073e4b --- /dev/null +++ b/cmd/cmdInit.go @@ -0,0 +1,52 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/skyline93/mysql-xtrabackup/internal/repository" + "github.com/spf13/cobra" +) + +var cmdInit = &cobra.Command{ + Use: "init -f config.json -p /data/backup -n repo1", + Short: "Init a repository", + Run: func(cmd *cobra.Command, args []string) { + configData, err := os.ReadFile(configFile) + if err != nil { + fmt.Printf("%s\n", err) + os.Exit(1) + } + + var config repository.Config + if err = json.Unmarshal(configData, &config); err != nil { + fmt.Printf("%s\n", err) + os.Exit(1) + } + + r := repository.NewRepository(repoName, &config) + if err := r.Init(rootPath); err != nil { + fmt.Printf("err: %s\n", err) + os.Exit(1) + } + }, +} + +var ( + configFile string + repoName string + rootPath string +) + +func init() { + cmdRoot.AddCommand(cmdInit) + + cmdInit.Flags().StringVarP(&configFile, "config", "f", "config.yaml", "config file path") + cmdInit.Flags().StringVarP(&repoName, "repo_name", "n", "", "repository name") + cmdInit.Flags().StringVarP(&rootPath, "path", "p", "", "repository path") + + cmdInit.MarkFlagRequired("config") + cmdInit.MarkFlagRequired("repo_name") + cmdInit.MarkFlagRequired("path") +} diff --git a/cmd/cmdList.go b/cmd/cmdList.go new file mode 100644 index 0000000..6292c72 --- /dev/null +++ b/cmd/cmdList.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "os" + + "github.com/pterm/pterm" + "github.com/skyline93/mysql-xtrabackup/internal/repository" + "github.com/spf13/cobra" +) + +var cmdList = &cobra.Command{ + Use: "list backupset -p /data/backup/repo1", + Short: "List backup sets in repository", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + os.Exit(0) + }, +} + +var cmdBackupSet = &cobra.Command{ + Use: "backupset", + Short: "backupset", + Run: func(cmd *cobra.Command, args []string) { + if err := listBackupSets(listOptions.RepoPath); err != nil { + fmt.Printf("%s", err) + os.Exit(1) + } + }, +} + +type ListOptions struct { + RepoPath string +} + +var listOptions ListOptions + +func init() { + cmdRoot.AddCommand(cmdList) + cmdList.AddCommand(cmdBackupSet) + + f := cmdBackupSet.Flags() + f.StringVarP(&listOptions.RepoPath, "repo_path", "p", "", "repo path") + + cmdBackupSet.MarkFlagRequired("repo_path") +} + +func listBackupSets(repoPath string) error { + repo := repository.Repository{} + if err := repository.LoadRepository(&repo, repoPath); err != nil { + return err + } + + backupSets, err := repo.ListBackupSets() + if err != nil { + return err + } + + items := pterm.TableData{{"BackupTime", "Id", "Type", "FromLSN", "ToLSN", "Size(Kb)"}} + for _, bs := range backupSets { + item := []string{bs.BackupTime, bs.Id, bs.Type, bs.FromLSN, bs.ToLSN, fmt.Sprintf("%d", bs.Size/1024)} + items = append(items, item) + } + + pterm.DefaultTable.WithHasHeader().WithData(items).Render() + + return nil +} diff --git a/cmd/cmdRestore.go b/cmd/cmdRestore.go new file mode 100644 index 0000000..c54fb2f --- /dev/null +++ b/cmd/cmdRestore.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "os" + + "github.com/skyline93/mysql-xtrabackup/internal/mysql" + "github.com/skyline93/mysql-xtrabackup/internal/repository" + "github.com/spf13/cobra" +) + +var cmdRestore = &cobra.Command{ + Use: "restore -p /data/backup/repo1 -m /usr/local/mysql/8.0.28 -t /data/restore/instance01 -i BACKUPSET_ID", + Short: "Restore a database from backupset", + Run: func(cmd *cobra.Command, args []string) { + repo := repository.Repository{} + if err := repository.LoadRepository(&repo, restoreOptions.RepoPath); err != nil { + fmt.Printf("load repo error: %s", err) + os.Exit(1) + } + + restorer := mysql.NewRestorer() + + err := restorer.Restore(&repo, restoreOptions.TargetPath, restoreOptions.MysqlPath, restoreOptions.BackupSetId) + if err != nil { + fmt.Printf("restore failed, err: %s", err) + os.Exit(1) + } + }, +} + +type RestoreOptions struct { + BackupSetId string + RepoPath string + TargetPath string + MysqlPath string +} + +var restoreOptions RestoreOptions + +func init() { + cmdRoot.AddCommand(cmdRestore) + + f := cmdRestore.Flags() + f.StringVarP(&restoreOptions.BackupSetId, "backupset_id", "i", "", "backup set id") + f.StringVarP(&restoreOptions.RepoPath, "repo_path", "p", "", "repo path") + f.StringVarP(&restoreOptions.TargetPath, "target_path", "t", "", "target path") + f.StringVarP(&restoreOptions.MysqlPath, "mysql_path", "m", "", "mysql path, example: /usr/local/mysql/8.0.28") + + cmdRestore.MarkFlagRequired("backupset_id") + cmdRestore.MarkFlagRequired("repo_path") + cmdRestore.MarkFlagRequired("target_path") + cmdRestore.MarkFlagRequired("mysql_path") +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..82566fc --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" +) + +var cmdRoot = &cobra.Command{ + Use: "easybackup", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + os.Exit(0) + }, +} + +func main() { + if err := cmdRoot.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..04c258d --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module github.com/skyline93/mysql-xtrabackup + +go 1.21 + +require ( + github.com/google/uuid v1.4.0 + github.com/pterm/pterm v0.12.74 + github.com/spf13/cobra v1.8.0 + gopkg.in/ini.v1 v1.67.0 +) + +require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect + github.com/containerd/console v1.0.3 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5525c15 --- /dev/null +++ b/go.sum @@ -0,0 +1,130 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.74 h1:fPsds9KisCyJh4NyY6bv8QJt3FLHceb5DxI6W0An9cc= +github.com/pterm/pterm v0.12.74/go.mod h1:+M33aZWQVpmLmLbvjykyGZ4gAfeebznRo8JMbabaxQU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/mysql/backup.go b/internal/mysql/backup.go new file mode 100644 index 0000000..6c1b2e9 --- /dev/null +++ b/internal/mysql/backup.go @@ -0,0 +1,161 @@ +package mysql + +import ( + "bufio" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/skyline93/mysql-xtrabackup/internal/repository" +) + +type Backuper struct { +} + +func NewBackuper() *Backuper { + return &Backuper{} +} + +func (b *Backuper) Backup(repo *repository.Repository, backupType string) (err error) { + bs := repository.NewBackupSet(backupType) + targetPath, err := filepath.Abs(filepath.Join(repo.DataPath(), bs.Id)) + if err != nil { + return err + } + + if _, err = os.Stat(targetPath); os.IsNotExist(err) { + err = os.MkdirAll(targetPath, 0755) + if err != nil { + return err + } + log.Printf("create path: %s", targetPath) + } + + defer func() { + if err != nil { + log.Printf("backup failed, err: %s", err) + os.RemoveAll(targetPath) + } + }() + + backupArgs := []string{ + filepath.Join(repo.Config.BinPath, "xtrabackup"), + "--backup", + fmt.Sprintf("--throttle=%d", repo.Config.Throttle), + fmt.Sprintf("--login-path=%s", repo.Config.LoginPath), + fmt.Sprintf("--datadir=%s", repo.Config.DataPath), + "--stream=xbstream", + } + + if repo.Config.TryCompress { + backupArgs = append(backupArgs, "--compress") + } + + if backupType == repository.TypeBackupSetIncr { + lastBackupSet, err := repo.GetLastBackupSet() + if err != nil { + return err + } + + backupArgs = append(backupArgs, fmt.Sprintf("--incremental-lsn=%s", lastBackupSet.ToLSN)) + } + + streamArgs := []string{ + "ssh", fmt.Sprintf("%s@%s", repo.Config.BackupUser, repo.Config.BackupHostName), + filepath.Join(repo.Config.BinPath, "xbstream"), "-x", "-C", targetPath, + } + + args := append(append(backupArgs, []string{"|"}...), streamArgs...) + + xtraLogPath, err := filepath.Abs(fmt.Sprintf("logs/xtrabackup-%s.log", time.Now().Format("20060102150405"))) + if err != nil { + return err + } + + log.Printf("log path: %s", xtraLogPath) + logFile, err := os.Create(xtraLogPath) + if err != nil { + return err + } + defer logFile.Close() + + backupTime := time.Now().Format("2006-01-02 15:04:05") + cmd := exec.Command("ssh", fmt.Sprintf("%s@%s", repo.Config.DbUser, repo.Config.DbHostName), strings.Join(args, " ")) + cmd.Stdout = logFile + cmd.Stderr = logFile + + log.Printf("cmd: %s", cmd.String()) + if err = cmd.Run(); err != nil { + return err + } + + content, err := os.ReadFile(filepath.Join(targetPath, "xtrabackup_checkpoints")) + if err != nil { + return err + } + + checkpoints, err := b.parseCheckpoints(string(content)) + if err != nil { + return err + } + + size, err := b.getBackupSize(targetPath) + if err != nil { + return err + } + + bs.FromLSN = checkpoints["from_lsn"] + bs.ToLSN = checkpoints["to_lsn"] + bs.Size = int64(size) + bs.BackupTime = backupTime + + if err = repo.AddBackupSet(bs); err != nil { + return err + } + + log.Printf("backup completed.\nbackupset: %s\nfrom_lsn: %s\nto_lsn: %s\nsize: %dbyte", bs.Id, bs.FromLSN, bs.ToLSN, bs.Size) + return nil +} + +func (b *Backuper) parseCheckpoints(content string) (map[string]string, error) { + checkpointsMap := make(map[string]string) + scanner := bufio.NewScanner(strings.NewReader(content)) + + for scanner.Scan() { + line := scanner.Text() + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + checkpointsMap[key] = value + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return checkpointsMap, nil +} + +func (b *Backuper) getBackupSize(targetPath string) (uint64, error) { + cmd := exec.Command("du", "-sb", targetPath) + + output, err := cmd.CombinedOutput() + if err != nil { + return 0, err + } + + // 解析 du 输出获取备份数据量 + fields := strings.Fields(string(output)) + size, err := strconv.ParseUint(fields[0], 10, 64) + if err != nil { + return 0, err + } + return size, nil +} diff --git a/internal/mysql/restore.go b/internal/mysql/restore.go new file mode 100644 index 0000000..02b498c --- /dev/null +++ b/internal/mysql/restore.go @@ -0,0 +1,194 @@ +package mysql + +import ( + "fmt" + "log" + "net" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/skyline93/mysql-xtrabackup/internal/repository" + "gopkg.in/ini.v1" +) + +type Restorer struct { +} + +func NewRestorer() *Restorer { + return &Restorer{} +} + +func (r *Restorer) Restore(repo *repository.Repository, targetPath string, mysqlPath string, backupSetId string) (err error) { + if _, err = os.Stat(targetPath); os.IsNotExist(err) { + if err = os.MkdirAll(targetPath, 0775); err != nil { + return + } + } + + defer func() { + if err != nil { + cmd := exec.Command("sudo", "rm", "-rf", targetPath) + log.Printf("run cmd: %s", cmd) + if err = cmd.Run(); err != nil { + return + } + } + }() + + var backupSets []repository.BackupSet + + backupSet, err := repo.GetBackupSet(backupSetId) + if err != nil { + return err + } + + if backupSet.Type == repository.TypeBackupSetFull { + backupSets = append(backupSets, *backupSet) + } else if backupSet.Type == repository.TypeBackupSetIncr { + backupSets, err = repo.GetBeforeBackupSet(backupSetId) + if err != nil { + return err + } + } + + var dataPath string + for _, bs := range backupSets { + targetSubPath := filepath.Join(targetPath, bs.Id) + + if bs.Type == repository.TypeBackupSetFull { + dataPath = targetSubPath + } + + // 拷贝文件 + cmd := exec.Command("cp", "-r", filepath.Join(repo.DataPath(), bs.Id), targetSubPath) + log.Printf("run cmd: %s", cmd) + if err = cmd.Run(); err != nil { + return err + } + + // 解压 + cmd = exec.Command( + filepath.Join(repo.Config.BinPath, "xtrabackup"), + "--decompress", "--remove-original", + fmt.Sprintf("--target-dir=%s", targetSubPath), + ) + log.Printf("run cmd: %s", cmd) + if err = cmd.Run(); err != nil { + return err + } + + // 追日志 + args := []string{ + "--prepare", "--apply-log-only", + fmt.Sprintf("--target-dir=%s", dataPath), + } + + if bs.Type == repository.TypeBackupSetIncr { + args = append(args, fmt.Sprintf("--incremental-dir=%s", targetSubPath)) + } + + cmd = exec.Command(filepath.Join(repo.Config.BinPath, "xtrabackup"), args...) + log.Printf("run cmd: %s", cmd) + if err = cmd.Run(); err != nil { + return err + } + + // 删除增量目标文件 + if bs.Type == repository.TypeBackupSetIncr { + if err = os.RemoveAll(targetSubPath); err != nil { + return err + } + } + } + + bkMyCnf, err := ini.Load(filepath.Join(dataPath, "backup-my.cnf")) + if err != nil { + return err + } + + sec := bkMyCnf.Section("mysqld") + + requiredKeys := []string{ + "innodb_data_file_path", + "innodb_log_files_in_group", + "innodb_log_file_size", + "innodb_page_size", + "innodb_undo_directory", + "innodb_undo_tablespaces", + "server_id", + "lower-case-table-names", + "log-bin", + } + + configs := make(map[string]string) + for _, k := range requiredKeys { + v, err := sec.GetKey(k) + if err != nil { + continue + } + + configs[k] = v.String() + } + + freePort, err := GetFreePort() + if err != nil { + return err + } + + configs["basedir"] = mysqlPath + configs["datadir"] = dataPath + configs["socket"] = filepath.Join(targetPath, "mysql.sock") + configs["pid-file"] = filepath.Join(targetPath, "mysql.pid") + configs["log-error"] = filepath.Join(targetPath, "mysql.err") + configs["port"] = fmt.Sprintf("%d", freePort) + + myCnf := ini.Empty(ini.LoadOptions{AllowBooleanKeys: true}) + sec, err = myCnf.NewSection("mysqld") + if err != nil { + return err + } + + for k, v := range configs { + _, err = sec.NewKey(k, v) + if err != nil { + return err + } + } + + configPath := filepath.Join(targetPath, "my.cnf") + if err = myCnf.SaveTo(configPath); err != nil { + return err + } + + cmd := exec.Command("sudo", "chown", "-R", "mysql:mysql", targetPath) + if err = cmd.Run(); err != nil { + return err + } + + cmd = exec.Command("sudo", "-u", "mysql", filepath.Join(mysqlPath, "bin/mysqld_safe"), fmt.Sprintf("--defaults-file=%s", configPath)) + log.Printf("run cmd: %s", cmd) + if err = cmd.Start(); err != nil { + return err + } + + time.Sleep(time.Second * 30) + + log.Printf("restore completed\nport: %d", freePort) + return nil +} + +func GetFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} diff --git a/internal/repository/config.go b/internal/repository/config.go new file mode 100644 index 0000000..24e1fd5 --- /dev/null +++ b/internal/repository/config.go @@ -0,0 +1,48 @@ +package repository + +import ( + "encoding/json" + "os" + "path/filepath" +) + +type Config struct { + Identifer string `json:"identifer"` + Version string `json:"version"` + LoginPath string `json:"login_path"` + DbHostName string `json:"db_hostname"` + DbUser string `json:"db_user"` + Throttle int `json:"throttle"` + TryCompress bool `json:"try_compress"` + + BinPath string `json:"bin_path"` + DataPath string `json:"data_path"` + BackupUser string `json:"backup_user"` + BackupHostName string `json:"backup_hostname"` +} + +func saveConfigToRepo(config *Config, repoPath string) error { + d, err := json.Marshal(config) + if err != nil { + return err + } + + if err = os.WriteFile(filepath.Join(repoPath, "config"), d, 0664); err != nil { + return err + } + + return nil +} + +func loadConfigFromRepo(config *Config, path string) error { + d, err := os.ReadFile(path) + if err != nil { + return err + } + + if err = json.Unmarshal(d, config); err != nil { + return err + } + + return nil +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..0792cc4 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,183 @@ +package repository + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + + "github.com/google/uuid" + "github.com/skyline93/mysql-xtrabackup/internal/stor" +) + +const ( + TypeBackupSetFull = "full" + TypeBackupSetIncr = "incr" +) + +type BackupSet struct { + Id string `json:"id"` + Type string `json:"type"` + FromLSN string `json:"from_lsn"` + ToLSN string `json:"to_lsn"` + Size int64 `json:"size"` + BackupTime string `json:"backup_time"` +} + +type Repository struct { + col *stor.Collection + Config *Config + Path string `json:"path"` + Name string `json:"name"` +} + +func NewBackupSet(backupSetType string) *BackupSet { + return &BackupSet{ + Id: uuid.New().String(), + Type: backupSetType, + } +} + +func NewRepository(name string, config *Config) *Repository { + return &Repository{ + col: stor.NewCollection(), + Name: name, + Config: config, + } +} + +func LoadRepository(repo *Repository, path string) error { + indexPath := filepath.Join(path, "index") + col := stor.Collection{} + + if err := stor.Deserialize(&col, indexPath); err != nil { + return err + } + + confPath := filepath.Join(path, "config") + conf := Config{} + if err := loadConfigFromRepo(&conf, confPath); err != nil { + return err + } + + repo.col = &col + repo.Config = &conf + repo.Name = conf.Identifer + repo.Path = path + + return nil +} + +func (r *Repository) Init(path string) error { + absPath, err := filepath.Abs(path) + if err != nil { + return err + } + + repoPath := filepath.Join(absPath, r.Name) + r.Path = repoPath + + if err = os.MkdirAll(repoPath, 0764); err != nil { + return err + } + + if err = os.MkdirAll(filepath.Join(r.Path, "data"), 0764); err != nil { + return err + } + + if err = saveConfigToRepo(r.Config, r.Path); err != nil { + return err + } + + if err = stor.Serialize(r.col, filepath.Join(r.Path, "index")); err != nil { + return err + } + + return nil +} + +func (r *Repository) AddBackupSet(backupSet *BackupSet) error { + v, err := json.Marshal(backupSet) + if err != nil { + return err + } + + if backupSet.Type == TypeBackupSetFull { + _, err := r.col.NewNode(backupSet.Id, v, true) + if err != nil { + return err + } + } else if backupSet.Type == TypeBackupSetIncr { + _, err := r.col.NewNode(backupSet.Id, v, false) + if err != nil { + return err + } + } + + if err := stor.Serialize(r.col, filepath.Join(r.Path, "index")); err != nil { + return err + } + + return nil +} + +func (r *Repository) GetBackupSet(backupSetId string) (*BackupSet, error) { + n := r.col.GetNode(backupSetId) + var backupSet BackupSet + if err := json.Unmarshal(n.Data, &backupSet); err != nil { + return nil, err + } + + return &backupSet, nil +} + +func (r *Repository) GetBeforeBackupSet(backupSetId string) ([]BackupSet, error) { + var backupSets []BackupSet + + nodes := r.col.GetBeforeNodes(backupSetId) + + for _, n := range nodes { + var backupSet BackupSet + if err := json.Unmarshal(n.Data, &backupSet); err != nil { + return nil, err + } + + backupSets = append(backupSets, backupSet) + } + + return backupSets, nil +} + +func (r *Repository) GetLastBackupSet() (*BackupSet, error) { + n := r.col.GetLastNode() + if n == nil { + return nil, errors.New("last backupset is not found") + } + + var backupSet BackupSet + if err := json.Unmarshal(n.Data, &backupSet); err != nil { + return nil, err + } + return &backupSet, nil +} + +func (r *Repository) DataPath() string { + return filepath.Join(r.Path, "data") +} + +func (r *Repository) ListBackupSets() ([]BackupSet, error) { + var backupSets []BackupSet + + ns := r.col.GetAllNodes() + + for _, n := range ns { + var backupSet BackupSet + if err := json.Unmarshal(n.Data, &backupSet); err != nil { + return nil, err + } + + backupSets = append(backupSets, backupSet) + } + + return backupSets, nil +} diff --git a/internal/stor/serialize.go b/internal/stor/serialize.go new file mode 100644 index 0000000..d873d54 --- /dev/null +++ b/internal/stor/serialize.go @@ -0,0 +1,90 @@ +package stor + +import ( + "encoding/json" + "os" +) + +type JsonNode struct { + Id string `json:"id"` + GroupId string `json:"group_id"` + Data []byte `json:"data"` +} + +type JsonGroup struct { + Id string `json:"id"` +} + +type JsonCollection struct { + Id string `json:"id"` + Nodes []JsonNode `json:"nodes"` + Groups []JsonGroup `json:"groups"` +} + +func Serialize(col *Collection, path string) error { + var nodes []JsonNode + var groups []JsonGroup + + for _, g := range col.Groups { + for _, n := range g.Nodes { + nodes = append(nodes, JsonNode{ + Id: n.Id, + GroupId: n.GroupId, + Data: n.Data, + }) + } + + groups = append(groups, JsonGroup{ + Id: g.Id, + }) + } + + c := &JsonCollection{ + Id: col.Id, + Nodes: nodes, + Groups: groups, + } + + d, err := json.Marshal(c) + if err != nil { + return err + } + + return os.WriteFile(path, d, 0664) +} + +func Deserialize(col *Collection, path string) error { + d, err := os.ReadFile(path) + if err != nil { + return err + } + + var jsonCol JsonCollection + if err := json.Unmarshal(d, &jsonCol); err != nil { + return err + } + + nodes := make(map[string]*Node) + + for _, j := range jsonCol.Groups { + g := &group{Id: j.Id, Nodes: make([]*Node, 0)} + + for _, n := range jsonCol.Nodes { + if n.GroupId == g.Id { + node := &Node{ + Id: n.Id, + GroupId: g.Id, + Data: n.Data, + } + g.addNode(node) + nodes[n.Id] = node + } + } + col.addGroup(g) + } + + col.Id = jsonCol.Id + col.nodes = nodes + + return nil +} diff --git a/internal/stor/stor.go b/internal/stor/stor.go new file mode 100644 index 0000000..2d67584 --- /dev/null +++ b/internal/stor/stor.go @@ -0,0 +1,138 @@ +package stor + +import ( + "errors" + + "github.com/google/uuid" +) + +type Node struct { + Id string + Prev *Node + Next *Node + Data []byte + GroupId string +} + +type group struct { + Id string + Prev *group + Next *group + Nodes []*Node +} + +type Collection struct { + Id string + Groups []*group + nodes map[string]*Node +} + +func newNode(id string, data []byte) *Node { + return &Node{Id: id, Data: data} +} + +func newGroup() *group { + return &group{ + Id: uuid.New().String(), + Nodes: make([]*Node, 0), + } +} + +func NewCollection() *Collection { + return &Collection{ + Id: uuid.New().String(), + Groups: make([]*group, 0), + nodes: make(map[string]*Node), + } +} + +func (g *group) addNode(node *Node) { + if len(g.Nodes) == 0 { + g.Id = node.Id + g.Nodes = append(g.Nodes, node) + } else { + tail := g.Nodes[len(g.Nodes)-1] + node.Prev = tail + tail.Next = node + g.Nodes = append(g.Nodes, node) + } + + node.GroupId = g.Id +} + +func (c *Collection) addGroup(g *group) { + if len(c.Groups) == 0 { + c.Groups = append(c.Groups, g) + } else { + tail := c.Groups[len(c.Groups)-1] + g.Prev = tail + tail.Next = g + c.Groups = append(c.Groups, g) + } +} + +func (c *Collection) NewNode(id string, data []byte, isNewGroup bool) (*Node, error) { + _, ok := c.nodes[id] + if ok { + return nil, errors.New("the id is exists already") + } + + node := newNode(id, data) + + if isNewGroup { + g := newGroup() + g.addNode(node) + c.addGroup(g) + } else { + c.Groups[len(c.Groups)-1].addNode(node) + } + + c.nodes[id] = node + return node, nil +} + +func (c *Collection) GetNode(nodeId string) *Node { + return c.nodes[nodeId] +} + +func (c *Collection) GetBeforeNodes(nodeId string) []*Node { + for _, g := range c.Groups { + var nodes []*Node + + for _, n := range g.Nodes { + nodes = append(nodes, n) + if n.Id == nodeId { + return nodes + } + } + } + + return nil +} + +func (c *Collection) GetStartNode(nodeId string) *Node { + for _, g := range c.Groups { + for _, n := range g.Nodes { + if n.Id == nodeId { + return g.Nodes[0] + } + } + } + + return nil +} + +func (c *Collection) GetLastNode() *Node { + g := c.Groups[len(c.Groups)-1] + return g.Nodes[len(g.Nodes)-1] +} + +func (c *Collection) GetAllNodes() []*Node { + var nodes []*Node + + for _, g := range c.Groups { + nodes = append(nodes, g.Nodes...) + } + + return nodes +}