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
+111 -35

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)")
+92 -20
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 {
@@ -49,6 +52,7 @@ type restoreModel struct {
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])
}