From 3bef3f76e95afeb32677059a55091e56c8cbad4a Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sat, 2 Aug 2025 16:58:12 +0700 Subject: [PATCH] updated create,restore commands for backup directory and added utils.go file --- .gitignore | 1 + cmd/create.go | 17 +----- cmd/restore.go | 140 +++++++++++++++++++++++++++++++++++++------------ cmd/utils.go | 16 ++++++ 4 files changed, 125 insertions(+), 49 deletions(-) create mode 100644 cmd/utils.go diff --git a/.gitignore b/.gitignore index 08cb523..f63a4a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ go.sum +backup diff --git a/cmd/create.go b/cmd/create.go index 0509398..e42b70c 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -162,7 +162,7 @@ func (m *model) startBackup() tea.Msg { m.progressCh <- -2 return } - + m.target += ".gz" // Обработка файлов и дисков srcFile, err := os.Open(m.source) if err != nil { @@ -171,7 +171,7 @@ func (m *model) startBackup() tea.Msg { } defer srcFile.Close() - dstFile, err := os.Create(m.target + ".gz") + dstFile, err := os.Create(m.target) if err != nil { m.progressCh <- -1 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() { rootCmd.AddCommand(createCmd) createCmd.Flags().StringVarP(&source, "source", "s", "", "Source file, directory or disk (e.g., /dev/sda) (required)") diff --git a/cmd/restore.go b/cmd/restore.go index 7a5a5ef..80ad794 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -1,11 +1,14 @@ package cmd import ( + "archive/tar" "compress/gzip" "fmt" "io" "os" + "path/filepath" "runtime" + "strings" "syscall" "time" @@ -23,7 +26,7 @@ var ( var restoreCmd = &cobra.Command{ Use: "restore", 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) { model, err := initialRestoreModel(restoreSource, restoreTarget) if err != nil { @@ -40,15 +43,16 @@ var restoreCmd = &cobra.Command{ } type restoreModel struct { - progress progress.Model - source string - target string - totalBytes int64 - processed int64 - done bool - err error - startTime time.Time - progressCh chan int64 + progress progress.Model + source string + target string + totalBytes int64 + processed int64 + done bool + err error + startTime time.Time + progressCh chan int64 + isDirectory bool } type restoreProgressMsg int64 @@ -66,13 +70,26 @@ func initialRestoreModel(src, dst string) (*restoreModel, error) { 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{ - progress: p, - source: src, - target: dst, - totalBytes: fileInfo.Size(), - startTime: time.Now(), - progressCh: make(chan int64, 100), + progress: p, + source: src, + target: targetPath, + totalBytes: fileInfo.Size(), + startTime: time.Now(), + progressCh: make(chan int64, 100), + isDirectory: isDirectory, }, nil } @@ -85,6 +102,16 @@ func (m *restoreModel) Init() tea.Cmd { func (m *restoreModel) startRestore() tea.Msg { 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) if err != nil { m.progressCh <- -1 @@ -92,14 +119,12 @@ func (m *restoreModel) startRestore() tea.Msg { } defer srcFile.Close() - // Создаем кастомный reader для отслеживания прогресса progressReader := &restoreProgressReader{ reader: srcFile, total: m.totalBytes, progressCh: m.progressCh, } - // Создаем gzip reader на основе кастомного reader'а gzipReader, err := gzip.NewReader(progressReader) if err != nil { m.progressCh <- -1 @@ -107,17 +132,14 @@ func (m *restoreModel) startRestore() tea.Msg { } defer gzipReader.Close() - // Определяем, является ли цель блочным устройством var dstFile *os.File if isBlockDevice(m.target) { - // Открываем блочное устройство для записи (без усечения!) dstFile, err = os.OpenFile(m.target, os.O_WRONLY|os.O_SYNC, 0) if err != nil { m.progressCh <- -1 return } } else { - // Обычный файл: создаем/усекаем dstFile, err = os.Create(m.target) if err != nil { m.progressCh <- -1 @@ -126,14 +148,12 @@ func (m *restoreModel) startRestore() tea.Msg { } defer dstFile.Close() - // Копируем распакованные данные в целевой файл/устройство _, err = io.Copy(dstFile, gzipReader) if err != nil { m.progressCh <- -1 return } - // Синхронизируем, чтобы все данные точно записались if err := dstFile.Sync(); err != nil { m.progressCh <- -1 return @@ -145,7 +165,61 @@ func (m *restoreModel) startRestore() tea.Msg { 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 { reader io.Reader total int64 @@ -190,21 +264,17 @@ func (m *restoreModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case restoreProgressMsg: - // Обновляем прогрессбар progressVal := float64(msg) / float64(m.totalBytes) if progressVal > 1.0 { progressVal = 1.0 } - // Создаем команду для обновления прогрессбара cmd := m.progress.SetPercent(progressVal) - // Продолжаем слушать обновления return m, tea.Batch(cmd, m.restoreProgressListener) case restoreDoneMsg: m.done = true - // Устанавливаем 100% и завершаем cmd := m.progress.SetPercent(1.0) return m, tea.Batch(cmd, tea.Quit) @@ -223,10 +293,14 @@ func (m *restoreModel) View() string { if m.done { duration := time.Since(m.startTime) - return fmt.Sprintf("\n✅ Restoration complete!\n\n"+ - "File: %s → %s\n"+ + restoreType := "File/Disk" + if m.isDirectory { + restoreType = "Directory" + } + return fmt.Sprintf("\n✅ %s restoration complete!\n\n"+ + "Archive: %s → %s\n"+ "Time: %v\n\n", - m.source, m.target, + restoreType, m.source, m.target, duration.Round(time.Millisecond)) } @@ -245,7 +319,6 @@ func (m *restoreModel) View() string { ) } -// isBlockDevice проверяет, является ли путь блочным устройством (только для Linux) func isBlockDevice(path string) bool { if runtime.GOOS != "linux" { return false @@ -267,7 +340,6 @@ func isBlockDevice(path string) bool { func init() { rootCmd.AddCommand(restoreCmd) 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("target") } diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..532486b --- /dev/null +++ b/cmd/utils.go @@ -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]) +}