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
+138 -18
+138 -18
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)
@@ -51,14 +53,15 @@ type model struct {
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")