updated create,restore commands for backup directory and added utils.go
file
This commit is contained in:
1 parent
7df9eeb8eb
commit
3bef3f76e9
4 files changed
+125
-49
No files matched your search
@@ -1 +1,2 @@
|
|||||||
go.sum
|
go.sum
|
||||||
|
backup
|
||||||
+2
-15
@@ -162,7 +162,7 @@ func (m *model) startBackup() tea.Msg {
|
|||||||
m.progressCh <- -2
|
m.progressCh <- -2
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
m.target += ".gz"
|
||||||
// Обработка файлов и дисков
|
// Обработка файлов и дисков
|
||||||
srcFile, err := os.Open(m.source)
|
srcFile, err := os.Open(m.source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -171,7 +171,7 @@ func (m *model) startBackup() tea.Msg {
|
|||||||
}
|
}
|
||||||
defer srcFile.Close()
|
defer srcFile.Close()
|
||||||
|
|
||||||
dstFile, err := os.Create(m.target + ".gz")
|
dstFile, err := os.Create(m.target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.progressCh <- -1
|
m.progressCh <- -1
|
||||||
return
|
return
|
||||||
@@ -377,19 +377,6 @@ func (m *model) View() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatBytes(b int64) string {
|
|
||||||
const unit = 1024
|
|
||||||
if b < unit {
|
|
||||||
return fmt.Sprintf("%d B", b)
|
|
||||||
}
|
|
||||||
div, exp := int64(unit), 0
|
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(createCmd)
|
rootCmd.AddCommand(createCmd)
|
||||||
createCmd.Flags().StringVarP(&source, "source", "s", "", "Source file, directory or disk (e.g., /dev/sda) (required)")
|
createCmd.Flags().StringVarP(&source, "source", "s", "", "Source file, directory or disk (e.g., /dev/sda) (required)")
|
||||||
|
|||||||
+106
-34
@@ -1,11 +1,14 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,7 +26,7 @@ var (
|
|||||||
var restoreCmd = &cobra.Command{
|
var restoreCmd = &cobra.Command{
|
||||||
Use: "restore",
|
Use: "restore",
|
||||||
Short: "Restore a backup",
|
Short: "Restore a backup",
|
||||||
Long: "Restore a backup from a gzip archive to a file or disk",
|
Long: "Restore a backup from an archive to a file, disk, or directory.",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
model, err := initialRestoreModel(restoreSource, restoreTarget)
|
model, err := initialRestoreModel(restoreSource, restoreTarget)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,15 +43,16 @@ var restoreCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type restoreModel struct {
|
type restoreModel struct {
|
||||||
progress progress.Model
|
progress progress.Model
|
||||||
source string
|
source string
|
||||||
target string
|
target string
|
||||||
totalBytes int64
|
totalBytes int64
|
||||||
processed int64
|
processed int64
|
||||||
done bool
|
done bool
|
||||||
err error
|
err error
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
progressCh chan int64
|
progressCh chan int64
|
||||||
|
isDirectory bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type restoreProgressMsg int64
|
type restoreProgressMsg int64
|
||||||
@@ -66,13 +70,26 @@ func initialRestoreModel(src, dst string) (*restoreModel, error) {
|
|||||||
progress.WithWidth(40),
|
progress.WithWidth(40),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
isDirectory := strings.HasSuffix(src, ".tar.gz")
|
||||||
|
targetPath := dst
|
||||||
|
|
||||||
|
if isDirectory && targetPath == "" {
|
||||||
|
targetPath, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get current directory: %w", err)
|
||||||
|
}
|
||||||
|
} else if !isDirectory && targetPath == "" {
|
||||||
|
return nil, fmt.Errorf("target flag is required for file or disk restoration")
|
||||||
|
}
|
||||||
|
|
||||||
return &restoreModel{
|
return &restoreModel{
|
||||||
progress: p,
|
progress: p,
|
||||||
source: src,
|
source: src,
|
||||||
target: dst,
|
target: targetPath,
|
||||||
totalBytes: fileInfo.Size(),
|
totalBytes: fileInfo.Size(),
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
progressCh: make(chan int64, 100),
|
progressCh: make(chan int64, 100),
|
||||||
|
isDirectory: isDirectory,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +102,16 @@ func (m *restoreModel) Init() tea.Cmd {
|
|||||||
|
|
||||||
func (m *restoreModel) startRestore() tea.Msg {
|
func (m *restoreModel) startRestore() tea.Msg {
|
||||||
go func() {
|
go func() {
|
||||||
|
if m.isDirectory {
|
||||||
|
if err := m.restoreDirectory(); err != nil {
|
||||||
|
m.progressCh <- -1 // Error signal
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.progressCh <- -2 // Done signal
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to original file/disk restoration logic
|
||||||
srcFile, err := os.Open(m.source)
|
srcFile, err := os.Open(m.source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.progressCh <- -1
|
m.progressCh <- -1
|
||||||
@@ -92,14 +119,12 @@ func (m *restoreModel) startRestore() tea.Msg {
|
|||||||
}
|
}
|
||||||
defer srcFile.Close()
|
defer srcFile.Close()
|
||||||
|
|
||||||
// Создаем кастомный reader для отслеживания прогресса
|
|
||||||
progressReader := &restoreProgressReader{
|
progressReader := &restoreProgressReader{
|
||||||
reader: srcFile,
|
reader: srcFile,
|
||||||
total: m.totalBytes,
|
total: m.totalBytes,
|
||||||
progressCh: m.progressCh,
|
progressCh: m.progressCh,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем gzip reader на основе кастомного reader'а
|
|
||||||
gzipReader, err := gzip.NewReader(progressReader)
|
gzipReader, err := gzip.NewReader(progressReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.progressCh <- -1
|
m.progressCh <- -1
|
||||||
@@ -107,17 +132,14 @@ func (m *restoreModel) startRestore() tea.Msg {
|
|||||||
}
|
}
|
||||||
defer gzipReader.Close()
|
defer gzipReader.Close()
|
||||||
|
|
||||||
// Определяем, является ли цель блочным устройством
|
|
||||||
var dstFile *os.File
|
var dstFile *os.File
|
||||||
if isBlockDevice(m.target) {
|
if isBlockDevice(m.target) {
|
||||||
// Открываем блочное устройство для записи (без усечения!)
|
|
||||||
dstFile, err = os.OpenFile(m.target, os.O_WRONLY|os.O_SYNC, 0)
|
dstFile, err = os.OpenFile(m.target, os.O_WRONLY|os.O_SYNC, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.progressCh <- -1
|
m.progressCh <- -1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Обычный файл: создаем/усекаем
|
|
||||||
dstFile, err = os.Create(m.target)
|
dstFile, err = os.Create(m.target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.progressCh <- -1
|
m.progressCh <- -1
|
||||||
@@ -126,14 +148,12 @@ func (m *restoreModel) startRestore() tea.Msg {
|
|||||||
}
|
}
|
||||||
defer dstFile.Close()
|
defer dstFile.Close()
|
||||||
|
|
||||||
// Копируем распакованные данные в целевой файл/устройство
|
|
||||||
_, err = io.Copy(dstFile, gzipReader)
|
_, err = io.Copy(dstFile, gzipReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.progressCh <- -1
|
m.progressCh <- -1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Синхронизируем, чтобы все данные точно записались
|
|
||||||
if err := dstFile.Sync(); err != nil {
|
if err := dstFile.Sync(); err != nil {
|
||||||
m.progressCh <- -1
|
m.progressCh <- -1
|
||||||
return
|
return
|
||||||
@@ -145,7 +165,61 @@ func (m *restoreModel) startRestore() tea.Msg {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// restoreProgressReader отслеживает прогресс чтения архива
|
func (m *restoreModel) restoreDirectory() error {
|
||||||
|
srcFile, err := os.Open(m.source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
progressReader := &restoreProgressReader{
|
||||||
|
reader: srcFile,
|
||||||
|
total: m.totalBytes,
|
||||||
|
progressCh: m.progressCh,
|
||||||
|
}
|
||||||
|
|
||||||
|
gzipReader, err := gzip.NewReader(progressReader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer gzipReader.Close()
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(gzipReader)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(m.target, header.Name)
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case tar.TypeReg:
|
||||||
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||||
|
outFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outFile.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type restoreProgressReader struct {
|
type restoreProgressReader struct {
|
||||||
reader io.Reader
|
reader io.Reader
|
||||||
total int64
|
total int64
|
||||||
@@ -190,21 +264,17 @@ func (m *restoreModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case restoreProgressMsg:
|
case restoreProgressMsg:
|
||||||
// Обновляем прогрессбар
|
|
||||||
progressVal := float64(msg) / float64(m.totalBytes)
|
progressVal := float64(msg) / float64(m.totalBytes)
|
||||||
if progressVal > 1.0 {
|
if progressVal > 1.0 {
|
||||||
progressVal = 1.0
|
progressVal = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем команду для обновления прогрессбара
|
|
||||||
cmd := m.progress.SetPercent(progressVal)
|
cmd := m.progress.SetPercent(progressVal)
|
||||||
|
|
||||||
// Продолжаем слушать обновления
|
|
||||||
return m, tea.Batch(cmd, m.restoreProgressListener)
|
return m, tea.Batch(cmd, m.restoreProgressListener)
|
||||||
|
|
||||||
case restoreDoneMsg:
|
case restoreDoneMsg:
|
||||||
m.done = true
|
m.done = true
|
||||||
// Устанавливаем 100% и завершаем
|
|
||||||
cmd := m.progress.SetPercent(1.0)
|
cmd := m.progress.SetPercent(1.0)
|
||||||
return m, tea.Batch(cmd, tea.Quit)
|
return m, tea.Batch(cmd, tea.Quit)
|
||||||
|
|
||||||
@@ -223,10 +293,14 @@ func (m *restoreModel) View() string {
|
|||||||
|
|
||||||
if m.done {
|
if m.done {
|
||||||
duration := time.Since(m.startTime)
|
duration := time.Since(m.startTime)
|
||||||
return fmt.Sprintf("\n✅ Restoration complete!\n\n"+
|
restoreType := "File/Disk"
|
||||||
"File: %s → %s\n"+
|
if m.isDirectory {
|
||||||
|
restoreType = "Directory"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\n✅ %s restoration complete!\n\n"+
|
||||||
|
"Archive: %s → %s\n"+
|
||||||
"Time: %v\n\n",
|
"Time: %v\n\n",
|
||||||
m.source, m.target,
|
restoreType, m.source, m.target,
|
||||||
duration.Round(time.Millisecond))
|
duration.Round(time.Millisecond))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +319,6 @@ func (m *restoreModel) View() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isBlockDevice проверяет, является ли путь блочным устройством (только для Linux)
|
|
||||||
func isBlockDevice(path string) bool {
|
func isBlockDevice(path string) bool {
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
return false
|
return false
|
||||||
@@ -267,7 +340,6 @@ func isBlockDevice(path string) bool {
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(restoreCmd)
|
rootCmd.AddCommand(restoreCmd)
|
||||||
restoreCmd.Flags().StringVarP(&restoreSource, "source", "s", "", "Source archive (required)")
|
restoreCmd.Flags().StringVarP(&restoreSource, "source", "s", "", "Source archive (required)")
|
||||||
restoreCmd.Flags().StringVarP(&restoreTarget, "target", "t", "", "Target file or disk (e.g., /dev/sdb) (required)")
|
restoreCmd.Flags().StringVarP(&restoreTarget, "target", "t", "", "Target file, disk, or directory. Defaults to current directory for archives.")
|
||||||
restoreCmd.MarkFlagRequired("source")
|
restoreCmd.MarkFlagRequired("source")
|
||||||
restoreCmd.MarkFlagRequired("target")
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func formatBytes(b int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if b < unit {
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
Reference in new issue
Block a user