added live time network backup and restore
This commit is contained in:
1 parent
3bef3f76e9
commit
1e3cc446e0
3 files changed
+276
-194
No files matched your search
+125
-97
@@ -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")
|
||||
}
|
||||
+142
-96
@@ -3,8 +3,10 @@ package cmd
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -26,7 +28,7 @@ var (
|
||||
var restoreCmd = &cobra.Command{
|
||||
Use: "restore",
|
||||
Short: "Restore a backup",
|
||||
Long: "Restore a backup from an archive to a file, disk, or directory.",
|
||||
Long: "Restore a backup from a local archive or network stream to a file, disk, or directory.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
model, err := initialRestoreModel(restoreSource, restoreTarget)
|
||||
if err != nil {
|
||||
@@ -43,16 +45,18 @@ 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
|
||||
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
|
||||
isNetworkSource bool
|
||||
statusMessage string
|
||||
}
|
||||
|
||||
type restoreProgressMsg int64
|
||||
@@ -60,36 +64,50 @@ type restoreDoneMsg struct{}
|
||||
type restoreErrorMsg struct{ err error }
|
||||
|
||||
func initialRestoreModel(src, dst string) (*restoreModel, error) {
|
||||
fileInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isNetwork := isNetworkAddress(src)
|
||||
p := progress.New(
|
||||
progress.WithDefaultGradient(),
|
||||
progress.WithWidth(40),
|
||||
)
|
||||
|
||||
isDirectory := strings.HasSuffix(src, ".tar.gz")
|
||||
var totalBytes int64 = 1 // Placeholder for network source, to avoid division by zero.
|
||||
var isDir bool
|
||||
targetPath := dst
|
||||
status := "Initializing..."
|
||||
|
||||
if isDirectory && targetPath == "" {
|
||||
targetPath, err = os.Getwd()
|
||||
if isNetwork {
|
||||
if targetPath == "" {
|
||||
return nil, fmt.Errorf("target flag -t is required for a network source")
|
||||
}
|
||||
status = fmt.Sprintf("Listening on %s...", src)
|
||||
} else {
|
||||
fileInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current directory: %w", err)
|
||||
return nil, fmt.Errorf("failed to stat source %s: %w", src, err)
|
||||
}
|
||||
totalBytes = fileInfo.Size()
|
||||
isDir = strings.HasSuffix(src, ".tar.gz")
|
||||
|
||||
if isDir && targetPath == "" {
|
||||
targetPath, err = os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current directory: %w", err)
|
||||
}
|
||||
} else if !isDir && targetPath == "" {
|
||||
return nil, fmt.Errorf("target flag -t is required for file or disk restoration")
|
||||
}
|
||||
} else if !isDirectory && targetPath == "" {
|
||||
return nil, fmt.Errorf("target flag is required for file or disk restoration")
|
||||
}
|
||||
|
||||
return &restoreModel{
|
||||
progress: p,
|
||||
source: src,
|
||||
target: targetPath,
|
||||
totalBytes: fileInfo.Size(),
|
||||
startTime: time.Now(),
|
||||
progressCh: make(chan int64, 100),
|
||||
isDirectory: isDirectory,
|
||||
progress: p,
|
||||
source: src,
|
||||
target: targetPath,
|
||||
totalBytes: totalBytes,
|
||||
startTime: time.Now(),
|
||||
progressCh: make(chan int64, 100),
|
||||
isDirectory: isDir,
|
||||
isNetworkSource: isNetwork,
|
||||
statusMessage: status,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -100,85 +118,108 @@ 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
|
||||
func (m *restoreModel) getSourceReader() (io.ReadCloser, error) {
|
||||
if m.isNetworkSource {
|
||||
listener, err := net.Listen("tcp", m.source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to listen on %s: %w", m.source, err)
|
||||
}
|
||||
defer listener.Close() // Close listener after accepting one connection
|
||||
|
||||
m.statusMessage = fmt.Sprintf("Waiting for connection on %s", m.source)
|
||||
// This is a blocking call
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to accept connection: %w", err)
|
||||
}
|
||||
m.statusMessage = "Client connected. Receiving data..."
|
||||
|
||||
// Protocol: Read 8 bytes for size, 1 byte for type
|
||||
if err := binary.Read(conn, binary.BigEndian, &m.totalBytes); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to read backup size: %w", err)
|
||||
}
|
||||
if m.totalBytes == 0 {
|
||||
m.totalBytes = 1 // Avoid division by zero for empty files
|
||||
}
|
||||
|
||||
// Fallback to original file/disk restoration logic
|
||||
srcFile, err := os.Open(m.source)
|
||||
typeByte := make([]byte, 1)
|
||||
if _, err := io.ReadFull(conn, typeByte); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to read backup type: %w", err)
|
||||
}
|
||||
m.isDirectory = (typeByte[0] == 1)
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Logic for local file
|
||||
m.statusMessage = fmt.Sprintf("Opening archive %s", m.source)
|
||||
return os.Open(m.source)
|
||||
}
|
||||
|
||||
func (m *restoreModel) startRestore() tea.Msg {
|
||||
go func() {
|
||||
sourceReader, err := m.getSourceReader()
|
||||
if err != nil {
|
||||
m.err = err
|
||||
m.progressCh <- -1
|
||||
return
|
||||
}
|
||||
defer srcFile.Close()
|
||||
defer sourceReader.Close()
|
||||
|
||||
progressReader := &restoreProgressReader{
|
||||
reader: srcFile,
|
||||
total: m.totalBytes,
|
||||
reader: sourceReader,
|
||||
progressCh: m.progressCh,
|
||||
}
|
||||
|
||||
gzipReader, err := gzip.NewReader(progressReader)
|
||||
if err != nil {
|
||||
m.progressCh <- -1
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
var restoreErr error
|
||||
if m.isDirectory {
|
||||
restoreErr = m.restoreDirectory(progressReader)
|
||||
} else {
|
||||
dstFile, err = os.Create(m.target)
|
||||
if err != nil {
|
||||
m.progressCh <- -1
|
||||
return
|
||||
}
|
||||
restoreErr = m.restoreFileOrDisk(progressReader)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
_, err = io.Copy(dstFile, gzipReader)
|
||||
if err != nil {
|
||||
if restoreErr != nil {
|
||||
m.err = restoreErr
|
||||
m.progressCh <- -1
|
||||
return
|
||||
}
|
||||
|
||||
if err := dstFile.Sync(); err != nil {
|
||||
m.progressCh <- -1
|
||||
return
|
||||
}
|
||||
|
||||
m.progressCh <- -2 // Сигнал завершения
|
||||
m.progressCh <- -2 // Done signal
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *restoreModel) restoreDirectory() error {
|
||||
srcFile, err := os.Open(m.source)
|
||||
func (m *restoreModel) restoreFileOrDisk(r io.Reader) error {
|
||||
gzipReader, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
defer gzipReader.Close()
|
||||
|
||||
progressReader := &restoreProgressReader{
|
||||
reader: srcFile,
|
||||
total: m.totalBytes,
|
||||
progressCh: m.progressCh,
|
||||
var dstFile *os.File
|
||||
if isBlockDevice(m.target) {
|
||||
dstFile, err = os.OpenFile(m.target, os.O_WRONLY|os.O_SYNC, 0)
|
||||
} else {
|
||||
if err := os.MkdirAll(filepath.Dir(m.target), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
dstFile, err = os.Create(m.target)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
gzipReader, err := gzip.NewReader(progressReader)
|
||||
if _, err = io.Copy(dstFile, gzipReader); err != nil {
|
||||
return err
|
||||
}
|
||||
return dstFile.Sync()
|
||||
}
|
||||
|
||||
func (m *restoreModel) restoreDirectory(r io.Reader) error {
|
||||
gzipReader, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -210,11 +251,14 @@ func (m *restoreModel) restoreDirectory() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
outFile.Close()
|
||||
return err
|
||||
_, copyErr := io.Copy(outFile, tarReader)
|
||||
closeErr := outFile.Close()
|
||||
if copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
outFile.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -222,7 +266,6 @@ func (m *restoreModel) restoreDirectory() error {
|
||||
|
||||
type restoreProgressReader struct {
|
||||
reader io.Reader
|
||||
total int64
|
||||
progressCh chan int64
|
||||
read int64
|
||||
}
|
||||
@@ -241,7 +284,10 @@ func (m *restoreModel) restoreProgressListener() tea.Msg {
|
||||
case n := <-m.progressCh:
|
||||
switch {
|
||||
case n == -1: // Ошибка
|
||||
return restoreErrorMsg{fmt.Errorf("restoration error")}
|
||||
if m.err == nil {
|
||||
m.err = fmt.Errorf("unknown restoration error")
|
||||
}
|
||||
return restoreErrorMsg{m.err}
|
||||
case n == -2: // Завершение
|
||||
return restoreDoneMsg{}
|
||||
default: // Прогресс
|
||||
@@ -268,9 +314,7 @@ func (m *restoreModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if progressVal > 1.0 {
|
||||
progressVal = 1.0
|
||||
}
|
||||
|
||||
cmd := m.progress.SetPercent(progressVal)
|
||||
|
||||
return m, tea.Batch(cmd, m.restoreProgressListener)
|
||||
|
||||
case restoreDoneMsg:
|
||||
@@ -282,7 +326,6 @@ func (m *restoreModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.err = msg.err
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -298,13 +341,19 @@ func (m *restoreModel) View() string {
|
||||
restoreType = "Directory"
|
||||
}
|
||||
return fmt.Sprintf("\n✅ %s restoration complete!\n\n"+
|
||||
"Archive: %s → %s\n"+
|
||||
"Source: %s\n"+
|
||||
"Target: %s\n"+
|
||||
"Time: %v\n\n",
|
||||
restoreType, m.source, m.target,
|
||||
duration.Round(time.Millisecond))
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("Restoring %s → %s", m.source, m.target)
|
||||
// Show status message before progress starts
|
||||
if m.processed == 0 && (m.isNetworkSource || m.statusMessage != "") {
|
||||
return m.statusMessage + "\n\nPress Ctrl+C to cancel"
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("Restoring from %s → %s", 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)
|
||||
@@ -323,23 +372,20 @@ func isBlockDevice(path string) bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
stat, ok := fileInfo.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return (stat.Mode & syscall.S_IFMT) == syscall.S_IFBLK
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(restoreCmd)
|
||||
restoreCmd.Flags().StringVarP(&restoreSource, "source", "s", "", "Source archive (required)")
|
||||
restoreCmd.Flags().StringVarP(&restoreTarget, "target", "t", "", "Target file, disk, or directory. Defaults to current directory for archives.")
|
||||
restoreCmd.Flags().StringVarP(&restoreSource, "source", "s", "", "Source archive or network address (e.g., /path/to/archive.tar.gz or 0.0.0.0:8080) (required)")
|
||||
restoreCmd.Flags().StringVarP(&restoreTarget, "target", "t", "", "Target file, disk, or directory. Required for network sources.")
|
||||
restoreCmd.MarkFlagRequired("source")
|
||||
}
|
||||
+9
-1
@@ -1,6 +1,14 @@
|
||||
package cmd
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isNetworkAddress checks if the address is in host:port format
|
||||
func isNetworkAddress(addr string) bool {
|
||||
return strings.Contains(addr, ":")
|
||||
}
|
||||
|
||||
func formatBytes(b int64) string {
|
||||
const unit = 1024
|
||||
|
||||
Reference in new issue
Block a user