added live time network backup and restore

This commit is contained in:
wt committed 2025-08-02 17:34:26 +07:00
1 parent 3bef3f76e9
commit 1e3cc446e0
3 files changed
+276 -194

No files matched your search

+125 -97
View File
@@ -3,8 +3,10 @@ package cmd
import (
"archive/tar"
"compress/gzip"
"encoding/binary"
"fmt"
"io"
"net"
"os"
"path/filepath"
"runtime"
@@ -44,23 +46,24 @@ var createCmd = &cobra.Command{
}
type model struct {
progress progress.Model
source string
target string
totalBytes int64
processed int64
done bool
err error
startTime time.Time
progressCh chan int64
isDirectory bool
progress progress.Model
source string
target string
totalBytes int64
processed int64
done bool
err error
startTime time.Time
progressCh chan int64
isDirectory bool
isNetworkTarget bool
}
type progressMsg int64
type doneMsg struct{}
type errorMsg struct{ err error }
// getFileSize возвращает размер файла, диска или директории
// getPathSize возвращает размер файла, диска или директории
func getPathSize(path string) (int64, error) {
fileInfo, err := os.Stat(path)
if err != nil {
@@ -121,26 +124,35 @@ func initialModel(src, dst string) (*model, error) {
progress.WithWidth(40),
)
// Проверяем, является ли источник директорией
srcInfo, err := os.Stat(src)
if err != nil {
return nil, err
}
// Добавляем расширение .tar.gz для директорий
isDirectory := srcInfo.IsDir()
networkTarget := isNetworkAddress(dst)
targetPath := dst
if srcInfo.IsDir() && filepath.Ext(targetPath) != ".tar.gz" {
targetPath += ".tar.gz"
if !networkTarget {
if isDirectory {
if filepath.Ext(targetPath) != ".tar.gz" {
targetPath += ".tar.gz"
}
} else { // Это файл или диск
if filepath.Ext(targetPath) != ".gz" {
targetPath += ".gz"
}
}
}
return &model{
progress: p,
source: src,
target: targetPath,
totalBytes: totalBytes,
startTime: time.Now(),
progressCh: make(chan int64, 100),
isDirectory: srcInfo.IsDir(),
progress: p,
source: src,
target: targetPath,
totalBytes: totalBytes,
startTime: time.Now(),
progressCh: make(chan int64, 100),
isDirectory: isDirectory,
isNetworkTarget: networkTarget,
}, nil
}
@@ -151,57 +163,58 @@ func (m *model) Init() tea.Cmd {
)
}
// getTargetWriter создает io.WriteCloser для файла или сетевого подключения
func (m *model) getTargetWriter() (io.WriteCloser, error) {
if m.isNetworkTarget {
conn, err := net.DialTimeout("tcp", m.target, 10*time.Second)
if err != nil {
return nil, fmt.Errorf("failed to connect to %s: %w", m.target, err)
}
// Протокол: сначала отправляем 8 байт (размер) и 1 байт (тип)
// 1. Общий размер (int64)
if err := binary.Write(conn, binary.BigEndian, m.totalBytes); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to send backup size: %w", err)
}
// 2. Тип бэкапа (1 = директория, 0 = файл/диск)
var typeByte byte = 0
if m.isDirectory {
typeByte = 1
}
if _, err := conn.Write([]byte{typeByte}); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to send backup type: %w", err)
}
return conn, nil
}
// Логика для локального файла
return os.Create(m.target)
}
func (m *model) startBackup() tea.Msg {
go func() {
targetWriter, err := m.getTargetWriter()
if err != nil {
m.err = err
m.progressCh <- -1
return
}
defer targetWriter.Close()
var backupErr error
if m.isDirectory {
err := m.backupDirectory()
if err != nil {
m.progressCh <- -1
return
}
m.progressCh <- -2
return
backupErr = m.backupDirectory(targetWriter)
} else {
backupErr = m.backupFileOrDisk(targetWriter)
}
m.target += ".gz"
// Обработка файлов и дисков
srcFile, err := os.Open(m.source)
if err != nil {
if backupErr != nil {
m.err = backupErr
m.progressCh <- -1
return
}
defer srcFile.Close()
dstFile, err := os.Create(m.target)
if err != nil {
m.progressCh <- -1
return
}
defer dstFile.Close()
gzipWriter := gzip.NewWriter(dstFile)
defer gzipWriter.Close()
buf := make([]byte, 32*1024) // 32KB buffer
for {
n, err := srcFile.Read(buf)
if n > 0 {
_, writeErr := gzipWriter.Write(buf[:n])
if writeErr != nil {
m.progressCh <- -1
return
}
m.progressCh <- int64(n)
}
if err == io.EOF {
break
}
if err != nil {
m.progressCh <- -1
return
}
}
m.progressCh <- -2 // Сигнал завершения
}()
@@ -209,14 +222,37 @@ func (m *model) startBackup() tea.Msg {
return nil
}
func (m *model) backupDirectory() error {
dstFile, err := os.Create(m.target)
func (m *model) backupFileOrDisk(w io.Writer) error {
srcFile, err := os.Open(m.source)
if err != nil {
return err
}
defer dstFile.Close()
defer srcFile.Close()
gzipWriter := gzip.NewWriter(dstFile)
gzipWriter := gzip.NewWriter(w)
defer gzipWriter.Close()
buf := make([]byte, 32*1024) // 32KB buffer
for {
n, err := srcFile.Read(buf)
if n > 0 {
if _, writeErr := gzipWriter.Write(buf[:n]); writeErr != nil {
return writeErr
}
m.progressCh <- int64(n)
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
}
func (m *model) backupDirectory(w io.Writer) error {
gzipWriter := gzip.NewWriter(w)
defer gzipWriter.Close()
tarWriter := tar.NewWriter(gzipWriter)
@@ -227,37 +263,31 @@ func (m *model) backupDirectory() error {
return err
}
// Создаем заголовок для файла/директории
header, err := tar.FileInfoHeader(fi, file)
if err != nil {
return err
}
// Устанавливаем относительный путь
relPath, err := filepath.Rel(m.source, file)
if err != nil {
return err
}
header.Name = relPath
// Пишем заголовок в архив
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
// Если это не обычный файл, пропускаем запись данных
if !fi.Mode().IsRegular() {
return nil
}
// Открываем файл для чтения
srcFile, err := os.Open(file)
if err != nil {
return err
}
defer srcFile.Close()
// Копируем содержимое файла в архив
buf := make([]byte, 32*1024)
for {
n, err := srcFile.Read(buf)
@@ -267,16 +297,13 @@ func (m *model) backupDirectory() error {
}
m.progressCh <- int64(n)
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
})
}
@@ -286,7 +313,7 @@ func (m *model) progressListener() tea.Msg {
case n := <-m.progressCh:
switch {
case n == -1: // Ошибка
return errorMsg{fmt.Errorf("backup error")}
return errorMsg{m.err}
case n == -2: // Завершение
return doneMsg{}
default: // Прогресс
@@ -309,21 +336,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case progressMsg:
// Обновляем прогрессбар
progressVal := float64(msg) / float64(m.totalBytes)
if progressVal > 1.0 {
progressVal = 1.0
}
// Создаем команду для обновления прогрессбара
cmd := m.progress.SetPercent(progressVal)
// Продолжаем слушать обновления
return m, tea.Batch(cmd, m.progressListener)
case doneMsg:
m.done = true
// Устанавливаем 100% и завершаем
cmd := m.progress.SetPercent(1.0)
return m, tea.Batch(cmd, tea.Quit)
@@ -342,27 +363,34 @@ func (m *model) View() string {
if m.done {
duration := time.Since(m.startTime)
ratio := float64(m.processed) / float64(m.totalBytes) * 100
backupType := "file/disk"
// ratio := float64(m.processed) / float64(m.totalBytes) * 100
backupType := "File/disk"
if m.isDirectory {
backupType = "directory"
backupType = "Directory"
}
if m.isNetworkTarget {
backupType += " network"
}
return fmt.Sprintf("\n✅ %s backup complete!\n\n"+
"Source: %s\n"+
"Backup: %s\n"+
"Size: %s → %s (%.1f%%)\n"+
"Target: %s\n"+
"Size: %s\n"+
"Time: %v\n\n",
backupType, m.source, m.target,
formatBytes(m.totalBytes), formatBytes(m.processed), ratio,
formatBytes(m.processed),
duration.Round(time.Millisecond))
}
backupType := "Backing up"
operation := "Backing up"
if m.isDirectory {
backupType = "Archiving"
operation = "Archiving"
}
title := fmt.Sprintf("%s %s → %s", backupType, m.source, m.target)
if m.isNetworkTarget {
operation = "Sending"
}
title := fmt.Sprintf("%s %s → %s", operation, m.source, m.target)
progressVal := float64(m.processed) / float64(m.totalBytes)
progressView := m.progress.ViewAs(progressVal)
stats := fmt.Sprintf("%s / %s (%.1f%%)", formatBytes(m.processed), formatBytes(m.totalBytes), progressVal*100)
@@ -380,7 +408,7 @@ func (m *model) View() string {
func init() {
rootCmd.AddCommand(createCmd)
createCmd.Flags().StringVarP(&source, "source", "s", "", "Source file, directory or disk (e.g., /dev/sda) (required)")
createCmd.Flags().StringVarP(&target, "target", "t", "", "Target backup file (required)")
createCmd.Flags().StringVarP(&target, "target", "t", "", "Target backup file or network address (e.g., /path/to/backup.tar.gz or 127.0.0.1:8080) (required)")
createCmd.MarkFlagRequired("source")
createCmd.MarkFlagRequired("target")
}