added backup directory to command create
This commit is contained in:
1 parent
91d4989531
commit
7df9eeb8eb
1 file changed
+152
-32
+152
-32
@@ -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")
|
||||||
|
|||||||
Reference in new issue
Block a user