added backup directory to command create
This commit is contained in:
+152
-32
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user