updated create,restore commands for backup directory and added utils.go

file
This commit is contained in:
wt
2025-08-02 16:58:12 +07:00
parent 7df9eeb8eb
commit 3bef3f76e9
4 changed files with 125 additions and 49 deletions
+1
View File
@@ -1 +1,2 @@
go.sum
backup
+2 -15
View File
@@ -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)")
+106 -34
View File
@@ -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")
}
+16
View File
@@ -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])
}