added backup directory to command create

This commit is contained in:
wt
2025-08-02 12:58:48 +07:00
parent 91d4989531
commit 7df9eeb8eb
+152 -32
View File
@@ -1,10 +1,12 @@
package cmd
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"syscall"
"time"
@@ -25,9 +27,9 @@ var (
var createCmd = &cobra.Command{
Use: "create",
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) {
model, err := initialModel(source, target+".gz")
model, err := initialModel(source, target)
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
@@ -42,23 +44,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
progress progress.Model
source string
target string
totalBytes int64
processed int64
done bool
err error
startTime time.Time
progressCh chan int64
isDirectory bool
}
type progressMsg int64
type doneMsg struct{}
type errorMsg struct{ err error }
// getFileSize возвращает размер файла или диска
func getFileSize(path string) (int64, error) {
// getFileSize возвращает размер файла, диска или директории
func getPathSize(path string) (int64, error) {
fileInfo, err := os.Stat(path)
if err != nil {
return 0, err
@@ -68,7 +71,6 @@ func getFileSize(path string) (int64, error) {
if runtime.GOOS == "linux" {
stat, ok := fileInfo.Sys().(*syscall.Stat_t)
if ok && (stat.Mode&syscall.S_IFMT) == syscall.S_IFBLK {
// Это блочное устройство - получаем размер через ioctl
f, err := os.Open(path)
if err != nil {
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
}
func initialModel(src, dst string) (*model, error) {
totalBytes, err := getFileSize(src)
totalBytes, err := getPathSize(src)
if err != nil {
return nil, err
}
@@ -104,26 +121,49 @@ func initialModel(src, dst string) (*model, error) {
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{
progress: p,
source: src,
target: dst,
totalBytes: totalBytes,
startTime: time.Now(),
progressCh: make(chan int64, 100),
progress: p,
source: src,
target: targetPath,
totalBytes: totalBytes,
startTime: time.Now(),
progressCh: make(chan int64, 100),
isDirectory: srcInfo.IsDir(),
}, nil
}
func (m *model) Init() tea.Cmd {
return tea.Batch(
m.startCompression,
m.startBackup,
m.progressListener,
)
}
func (m *model) startCompression() tea.Msg {
// Переносим всю работу с файлами внутрь горутины
func (m *model) startBackup() tea.Msg {
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)
if err != nil {
m.progressCh <- -1
@@ -131,7 +171,7 @@ func (m *model) startCompression() tea.Msg {
}
defer srcFile.Close()
dstFile, err := os.Create(m.target)
dstFile, err := os.Create(m.target + ".gz")
if err != nil {
m.progressCh <- -1
return
@@ -141,7 +181,6 @@ func (m *model) startCompression() tea.Msg {
gzipWriter := gzip.NewWriter(dstFile)
defer gzipWriter.Close()
// Для блочных устройств используем прямой доступ
buf := make([]byte, 32*1024) // 32KB buffer
for {
n, err := srcFile.Read(buf)
@@ -170,12 +209,84 @@ func (m *model) startCompression() tea.Msg {
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 {
select {
case n := <-m.progressCh:
switch {
case n == -1: // Ошибка
return errorMsg{fmt.Errorf("compression error")}
return errorMsg{fmt.Errorf("backup error")}
case n == -2: // Завершение
return doneMsg{}
default: // Прогресс
@@ -232,17 +343,26 @@ func (m *model) View() string {
if m.done {
duration := time.Since(m.startTime)
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"+
"Backup: %s\n"+
"Size: %s → %s (%.1f%%)\n"+
"Time: %v\n\n",
m.source, m.target,
backupType, m.source, m.target,
formatBytes(m.totalBytes), formatBytes(m.processed), ratio,
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)
progressView := m.progress.ViewAs(progressVal)
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() {
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.MarkFlagRequired("source")
createCmd.MarkFlagRequired("target")