added backup directory to command create

This commit is contained in:
wt committed 2025-08-02 12:58:48 +07:00
1 parent 91d4989531
commit 7df9eeb8eb
1 file changed
+152 -32
+152 -32
View File
@@ -1,10 +1,12 @@
package cmd package cmd
import ( import (
"archive/tar"
"compress/gzip" "compress/gzip"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"runtime" "runtime"
"syscall" "syscall"
"time" "time"
@@ -25,9 +27,9 @@ var (
var createCmd = &cobra.Command{ var createCmd = &cobra.Command{
Use: "create", Use: "create",
Short: "Create a new backup", Short: "Create a new backup",
Long: "Create a new backup of a file or disk", Long: "Create a new backup of a file, directory, or disk",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
model, err := initialModel(source, target+".gz") model, err := initialModel(source, target)
if err != nil { if err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
os.Exit(1) os.Exit(1)
@@ -42,23 +44,24 @@ var createCmd = &cobra.Command{
} }
type model struct { type model 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 progressMsg int64 type progressMsg int64
type doneMsg struct{} type doneMsg struct{}
type errorMsg struct{ err error } type errorMsg struct{ err error }
// getFileSize возвращает размер файла или диска // getFileSize возвращает размер файла, диска или директории
func getFileSize(path string) (int64, error) { func getPathSize(path string) (int64, error) {
fileInfo, err := os.Stat(path) fileInfo, err := os.Stat(path)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -68,7 +71,6 @@ func getFileSize(path string) (int64, error) {
if runtime.GOOS == "linux" { if runtime.GOOS == "linux" {
stat, ok := fileInfo.Sys().(*syscall.Stat_t) stat, ok := fileInfo.Sys().(*syscall.Stat_t)
if ok && (stat.Mode&syscall.S_IFMT) == syscall.S_IFBLK { if ok && (stat.Mode&syscall.S_IFMT) == syscall.S_IFBLK {
// Это блочное устройство - получаем размер через ioctl
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -89,12 +91,27 @@ func getFileSize(path string) (int64, error) {
} }
} }
// Для обычных файлов и других систем // Для директорий
if fileInfo.IsDir() {
var total int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
total += info.Size()
}
return nil
})
return total, err
}
// Для обычных файлов
return fileInfo.Size(), nil return fileInfo.Size(), nil
} }
func initialModel(src, dst string) (*model, error) { func initialModel(src, dst string) (*model, error) {
totalBytes, err := getFileSize(src) totalBytes, err := getPathSize(src)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -104,26 +121,49 @@ func initialModel(src, dst string) (*model, error) {
progress.WithWidth(40), progress.WithWidth(40),
) )
// Проверяем, является ли источник директорией
srcInfo, err := os.Stat(src)
if err != nil {
return nil, err
}
// Добавляем расширение .tar.gz для директорий
targetPath := dst
if srcInfo.IsDir() && filepath.Ext(targetPath) != ".tar.gz" {
targetPath += ".tar.gz"
}
return &model{ return &model{
progress: p, progress: p,
source: src, source: src,
target: dst, target: targetPath,
totalBytes: totalBytes, totalBytes: totalBytes,
startTime: time.Now(), startTime: time.Now(),
progressCh: make(chan int64, 100), progressCh: make(chan int64, 100),
isDirectory: srcInfo.IsDir(),
}, nil }, nil
} }
func (m *model) Init() tea.Cmd { func (m *model) Init() tea.Cmd {
return tea.Batch( return tea.Batch(
m.startCompression, m.startBackup,
m.progressListener, m.progressListener,
) )
} }
func (m *model) startCompression() tea.Msg { func (m *model) startBackup() tea.Msg {
// Переносим всю работу с файлами внутрь горутины
go func() { go func() {
if m.isDirectory {
err := m.backupDirectory()
if err != nil {
m.progressCh <- -1
return
}
m.progressCh <- -2
return
}
// Обработка файлов и дисков
srcFile, err := os.Open(m.source) srcFile, err := os.Open(m.source)
if err != nil { if err != nil {
m.progressCh <- -1 m.progressCh <- -1
@@ -131,7 +171,7 @@ func (m *model) startCompression() tea.Msg {
} }
defer srcFile.Close() defer srcFile.Close()
dstFile, err := os.Create(m.target) dstFile, err := os.Create(m.target + ".gz")
if err != nil { if err != nil {
m.progressCh <- -1 m.progressCh <- -1
return return
@@ -141,7 +181,6 @@ func (m *model) startCompression() tea.Msg {
gzipWriter := gzip.NewWriter(dstFile) gzipWriter := gzip.NewWriter(dstFile)
defer gzipWriter.Close() defer gzipWriter.Close()
// Для блочных устройств используем прямой доступ
buf := make([]byte, 32*1024) // 32KB buffer buf := make([]byte, 32*1024) // 32KB buffer
for { for {
n, err := srcFile.Read(buf) n, err := srcFile.Read(buf)
@@ -170,12 +209,84 @@ func (m *model) startCompression() tea.Msg {
return nil return nil
} }
func (m *model) backupDirectory() error {
dstFile, err := os.Create(m.target)
if err != nil {
return err
}
defer dstFile.Close()
gzipWriter := gzip.NewWriter(dstFile)
defer gzipWriter.Close()
tarWriter := tar.NewWriter(gzipWriter)
defer tarWriter.Close()
return filepath.Walk(m.source, func(file string, fi os.FileInfo, err error) error {
if err != nil {
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)
if n > 0 {
if _, err := tarWriter.Write(buf[:n]); err != nil {
return err
}
m.progressCh <- int64(n)
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
})
}
func (m *model) progressListener() tea.Msg { func (m *model) progressListener() tea.Msg {
select { select {
case n := <-m.progressCh: case n := <-m.progressCh:
switch { switch {
case n == -1: // Ошибка case n == -1: // Ошибка
return errorMsg{fmt.Errorf("compression error")} return errorMsg{fmt.Errorf("backup error")}
case n == -2: // Завершение case n == -2: // Завершение
return doneMsg{} return doneMsg{}
default: // Прогресс default: // Прогресс
@@ -232,17 +343,26 @@ func (m *model) View() string {
if m.done { if m.done {
duration := time.Since(m.startTime) duration := time.Since(m.startTime)
ratio := float64(m.processed) / float64(m.totalBytes) * 100 ratio := float64(m.processed) / float64(m.totalBytes) * 100
return fmt.Sprintf("\n✅ Backup complete!\n\n"+ backupType := "file/disk"
if m.isDirectory {
backupType = "directory"
}
return fmt.Sprintf("\n✅ %s backup complete!\n\n"+
"Source: %s\n"+ "Source: %s\n"+
"Backup: %s\n"+ "Backup: %s\n"+
"Size: %s → %s (%.1f%%)\n"+ "Size: %s → %s (%.1f%%)\n"+
"Time: %v\n\n", "Time: %v\n\n",
m.source, m.target, backupType, m.source, m.target,
formatBytes(m.totalBytes), formatBytes(m.processed), ratio, formatBytes(m.totalBytes), formatBytes(m.processed), ratio,
duration.Round(time.Millisecond)) duration.Round(time.Millisecond))
} }
title := fmt.Sprintf("Backing up %s → %s", m.source, m.target) backupType := "Backing up"
if m.isDirectory {
backupType = "Archiving"
}
title := fmt.Sprintf("%s %s → %s", backupType, m.source, m.target)
progressVal := float64(m.processed) / float64(m.totalBytes) progressVal := float64(m.processed) / float64(m.totalBytes)
progressView := m.progress.ViewAs(progressVal) progressView := m.progress.ViewAs(progressVal)
stats := fmt.Sprintf("%s / %s (%.1f%%)", formatBytes(m.processed), formatBytes(m.totalBytes), progressVal*100) stats := fmt.Sprintf("%s / %s (%.1f%%)", formatBytes(m.processed), formatBytes(m.totalBytes), progressVal*100)
@@ -272,7 +392,7 @@ func formatBytes(b int64) string {
func init() { func init() {
rootCmd.AddCommand(createCmd) rootCmd.AddCommand(createCmd)
createCmd.Flags().StringVarP(&source, "source", "s", "", "Source file or disk (e.g., /dev/sda) (required)") 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 (required)")
createCmd.MarkFlagRequired("source") createCmd.MarkFlagRequired("source")
createCmd.MarkFlagRequired("target") createCmd.MarkFlagRequired("target")