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

file
This commit is contained in:
wt committed 2025-08-02 16:58:12 +07:00
1 parent 7df9eeb8eb
commit 3bef3f76e9
4 files changed
+125 -49

No files matched your search

+1
View File
@@ -1 +1,2 @@
go.sum go.sum
backup
+2 -15
View File
@@ -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
View File
@@ -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")
} }
+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])
}