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

+142 -96
View File
@@ -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")
}